commit
83477497c1
@ -55,6 +55,7 @@ bazel-testlogs
|
||||
.DS_Store
|
||||
cmake-build-debug
|
||||
dist
|
||||
jacoco.exec
|
||||
tmp
|
||||
|
||||
# VP9 extension
|
||||
@ -64,6 +65,7 @@ extensions/vp9/src/main/jni/libyuv
|
||||
|
||||
# AV1 extension
|
||||
extensions/av1/src/main/jni/libgav1
|
||||
extensions/av1/src/main/jni/cpu_features
|
||||
|
||||
# Opus extension
|
||||
extensions/opus/src/main/jni/libopus
|
||||
|
@ -1,4 +1,4 @@
|
||||
# ExoPlayer #
|
||||
# ExoPlayer <img src="https://img.shields.io/github/v/release/google/ExoPlayer.svg?label=latest"/> #
|
||||
|
||||
ExoPlayer is an application level media player for Android. It provides an
|
||||
alternative to Android’s MediaPlayer API for playing audio and video both
|
||||
|
4658
RELEASENOTES.md
4658
RELEASENOTES.md
File diff suppressed because it is too large
Load Diff
@ -17,15 +17,16 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath 'com.novoda:bintray-release:0.9.1'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0'
|
||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1'
|
||||
}
|
||||
}
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
|
||||
}
|
||||
project.ext {
|
||||
exoplayerPublishEnabled = false
|
||||
|
34
common_library_config.gradle
Normal file
34
common_library_config.gradle
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright (C) 2020 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: "$gradle.ext.exoplayerSettingsDir/constants.gradle"
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2017 The Android Open Source Project
|
||||
// Copyright 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.
|
||||
@ -13,30 +13,33 @@
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.11.8'
|
||||
releaseVersionCode = 2011008
|
||||
releaseVersion = '2.12.0'
|
||||
releaseVersionCode = 2012000
|
||||
minSdkVersion = 16
|
||||
appTargetSdkVersion = 29
|
||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
|
||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
||||
compileSdkVersion = 29
|
||||
dexmakerVersion = '2.21.0'
|
||||
guavaVersion = '23.5-android'
|
||||
mockitoVersion = '2.25.0'
|
||||
robolectricVersion = '4.3'
|
||||
autoValueVersion = '1.6'
|
||||
autoServiceVersion = '1.0-rc4'
|
||||
checkerframeworkVersion = '2.5.0'
|
||||
junitVersion = '4.13-rc-2'
|
||||
guavaVersion = '27.1-android'
|
||||
mockitoVersion = '2.28.2'
|
||||
mockWebServerVersion = '3.12.0'
|
||||
robolectricVersion = '4.4-SNAPSHOT'
|
||||
checkerframeworkVersion = '3.3.0'
|
||||
checkerframeworkCompatVersion = '2.5.0'
|
||||
jsr305Version = '3.0.2'
|
||||
kotlinAnnotationsVersion = '1.3.31'
|
||||
kotlinAnnotationsVersion = '1.3.70'
|
||||
androidxAnnotationVersion = '1.1.0'
|
||||
androidxAppCompatVersion = '1.1.0'
|
||||
androidxCollectionVersion = '1.1.0'
|
||||
androidxMediaVersion = '1.0.1'
|
||||
androidxMultidexVersion = '2.0.0'
|
||||
androidxRecyclerViewVersion = '1.1.0'
|
||||
androidxTestCoreVersion = '1.2.0'
|
||||
androidxTestJUnitVersion = '1.1.1'
|
||||
androidxTestRunnerVersion = '1.2.0'
|
||||
androidxTestRulesVersion = '1.2.0'
|
||||
truthVersion = '0.44'
|
||||
truthVersion = '1.0'
|
||||
modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
|
@ -12,18 +12,25 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
def rootDir = gradle.ext.exoplayerRoot
|
||||
if (!gradle.ext.has('exoplayerSettingsDir')) {
|
||||
gradle.ext.exoplayerSettingsDir =
|
||||
new File(rootDir.toString()).getCanonicalPath()
|
||||
}
|
||||
def modulePrefix = ':'
|
||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||
}
|
||||
|
||||
include modulePrefix + 'library'
|
||||
include modulePrefix + 'library-common'
|
||||
include modulePrefix + 'library-core'
|
||||
include modulePrefix + 'library-dash'
|
||||
include modulePrefix + 'library-extractor'
|
||||
include modulePrefix + 'library-hls'
|
||||
include modulePrefix + 'library-smoothstreaming'
|
||||
include modulePrefix + 'library-ui'
|
||||
include modulePrefix + 'testutils'
|
||||
include modulePrefix + 'testdata'
|
||||
include modulePrefix + 'extension-av1'
|
||||
include modulePrefix + 'extension-ffmpeg'
|
||||
include modulePrefix + 'extension-flac'
|
||||
@ -32,6 +39,7 @@ include modulePrefix + 'extension-ima'
|
||||
include modulePrefix + 'extension-cast'
|
||||
include modulePrefix + 'extension-cronet'
|
||||
include modulePrefix + 'extension-mediasession'
|
||||
include modulePrefix + 'extension-media2'
|
||||
include modulePrefix + 'extension-okhttp'
|
||||
include modulePrefix + 'extension-opus'
|
||||
include modulePrefix + 'extension-vp9'
|
||||
@ -41,12 +49,15 @@ include modulePrefix + 'extension-jobdispatcher'
|
||||
include modulePrefix + 'extension-workmanager'
|
||||
|
||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
||||
project(modulePrefix + 'library-common').projectDir = new File(rootDir, 'library/common')
|
||||
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
|
||||
project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')
|
||||
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
|
||||
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
|
||||
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
|
||||
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
|
||||
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
|
||||
project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata')
|
||||
project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1')
|
||||
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
|
||||
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
|
||||
@ -55,6 +66,7 @@ project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensio
|
||||
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
|
||||
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
|
||||
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
|
||||
project(modulePrefix + 'extension-media2').projectDir = new File(rootDir, 'extensions/media2')
|
||||
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
|
||||
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
|
||||
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
|
||||
|
@ -27,6 +27,7 @@ android {
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -57,8 +58,9 @@ dependencies {
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'extension-cast')
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2020 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 androidx.multidex.MultiDexApplication;
|
||||
|
||||
// Note: Multidex is enabled in code not AndroidManifest.xml because the internal build system
|
||||
// doesn't dejetify MultiDexApplication in AndroidManifest.xml.
|
||||
/** Application for multidex support. */
|
||||
public final class DemoApplication extends MultiDexApplication {}
|
@ -17,8 +17,8 @@ package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@ -42,19 +42,19 @@ import java.util.List;
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
|
||||
.setTitle("Clear DASH: Tears")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear DASH: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
|
||||
.setTitle("Clear HLS: Angel one")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear HLS: Angel one").build())
|
||||
.setMimeType(MIME_TYPE_HLS)
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri("https://html5demos.com/assets/dizzy.mp4")
|
||||
.setTitle("Clear MP4: Dizzy")
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle("Clear MP4: Dizzy").build())
|
||||
.setMimeType(MIME_TYPE_VIDEO_MP4)
|
||||
.build());
|
||||
|
||||
@ -62,39 +62,29 @@ import java.util.List;
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"))
|
||||
.setTitle("Widevine DASH cenc: Tears")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cenc: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||
Collections.emptyMap()))
|
||||
.setDrmUuid(C.WIDEVINE_UUID)
|
||||
.setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri(
|
||||
Uri.parse(
|
||||
"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"))
|
||||
.setTitle("Widevine DASH cbc1: Tears")
|
||||
.setUri("https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cbc1: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||
Collections.emptyMap()))
|
||||
.setDrmUuid(C.WIDEVINE_UUID)
|
||||
.setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build());
|
||||
samples.add(
|
||||
new MediaItem.Builder()
|
||||
.setUri(
|
||||
Uri.parse(
|
||||
"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"))
|
||||
.setTitle("Widevine DASH cbcs: Tears")
|
||||
.setUri("https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder().setTitle("Widevine DASH cbcs: Tears").build())
|
||||
.setMimeType(MIME_TYPE_DASH)
|
||||
.setDrmConfiguration(
|
||||
new DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||
Collections.emptyMap()))
|
||||
.setDrmUuid(C.WIDEVINE_UUID)
|
||||
.setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||
.build());
|
||||
|
||||
SAMPLES = Collections.unmodifiableList(samples);
|
||||
|
@ -37,10 +37,12 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.dynamite.DynamiteModule;
|
||||
@ -171,8 +173,6 @@ public class MainActivity extends AppCompatActivity
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
} else {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,6 +199,7 @@ public class MainActivity extends AppCompatActivity
|
||||
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
TextView v = (TextView) LayoutInflater.from(parent.getContext())
|
||||
.inflate(android.R.layout.simple_list_item_1, parent, false);
|
||||
@ -207,9 +208,10 @@ public class MainActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
||||
holder.item = playerManager.getItem(position);
|
||||
holder.item = Assertions.checkNotNull(playerManager.getItem(position));
|
||||
|
||||
TextView view = holder.textView;
|
||||
view.setText(holder.item.title);
|
||||
view.setText(holder.item.mediaMetadata.title);
|
||||
// TODO: Solve coloring using the theme's ColorStateList.
|
||||
view.setTextColor(
|
||||
ColorUtils.setAlphaComponent(
|
||||
@ -236,7 +238,9 @@ public class MainActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin,
|
||||
public boolean onMove(
|
||||
@NonNull RecyclerView list,
|
||||
RecyclerView.ViewHolder origin,
|
||||
RecyclerView.ViewHolder target) {
|
||||
int fromPosition = origin.getAdapterPosition();
|
||||
int toPosition = target.getAdapterPosition();
|
||||
@ -261,7 +265,7 @@ public class MainActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
if (draggingFromPosition != C.INDEX_UNSET) {
|
||||
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||
@ -300,11 +304,11 @@ public class MainActivity extends AppCompatActivity
|
||||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@NonNull
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
((TextView) view).setText(getItem(position).title);
|
||||
((TextView) view).setText(Util.castNonNull(getItem(position)).mediaMetadata.title);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
@ -16,45 +16,28 @@
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||
import com.google.android.exoplayer2.Player.EventListener;
|
||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||
import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItemConverter;
|
||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
/** Manages players and an internal media queue for the demo app. */
|
||||
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
|
||||
@ -84,8 +67,6 @@ import java.util.Map;
|
||||
private final CastPlayer castPlayer;
|
||||
private final ArrayList<MediaItem> mediaQueue;
|
||||
private final Listener listener;
|
||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||
private final MediaItemConverter mediaItemConverter;
|
||||
|
||||
private TrackGroupArray lastSeenTrackGroupArray;
|
||||
private int currentItemIndex;
|
||||
@ -111,8 +92,6 @@ import java.util.Map;
|
||||
this.castControlView = castControlView;
|
||||
mediaQueue = new ArrayList<>();
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||
mediaItemConverter = new DefaultMediaItemConverter();
|
||||
|
||||
trackSelector = new DefaultTrackSelector(context);
|
||||
exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
|
||||
@ -135,7 +114,7 @@ import java.util.Map;
|
||||
* @param itemIndex The index of the item to play.
|
||||
*/
|
||||
public void selectQueueItem(int itemIndex) {
|
||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
||||
setCurrentItem(itemIndex);
|
||||
}
|
||||
|
||||
/** Returns the index of the currently played item. */
|
||||
@ -150,10 +129,7 @@ import java.util.Map;
|
||||
*/
|
||||
public void addItem(MediaItem item) {
|
||||
mediaQueue.add(item);
|
||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
||||
if (currentPlayer == castPlayer) {
|
||||
castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
|
||||
}
|
||||
currentPlayer.addMediaItem(item);
|
||||
}
|
||||
|
||||
/** Returns the size of the media queue. */
|
||||
@ -182,16 +158,7 @@ import java.util.Map;
|
||||
if (itemIndex == -1) {
|
||||
return false;
|
||||
}
|
||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
||||
if (currentPlayer == castPlayer) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
currentPlayer.removeMediaItem(itemIndex);
|
||||
mediaQueue.remove(itemIndex);
|
||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
||||
@ -205,34 +172,25 @@ import java.util.Map;
|
||||
* Moves an item within the queue.
|
||||
*
|
||||
* @param item The item to move.
|
||||
* @param toIndex The target index of the item in the queue.
|
||||
* @param newIndex The target index of the item in the queue.
|
||||
* @return Whether the item move was successful.
|
||||
*/
|
||||
public boolean moveItem(MediaItem item, int toIndex) {
|
||||
public boolean moveItem(MediaItem item, int newIndex) {
|
||||
int fromIndex = mediaQueue.indexOf(item);
|
||||
if (fromIndex == -1) {
|
||||
return false;
|
||||
}
|
||||
// Player update.
|
||||
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||
if (currentPlayer == castPlayer && 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));
|
||||
// Player update.
|
||||
currentPlayer.moveMediaItem(fromIndex, newIndex);
|
||||
mediaQueue.add(newIndex, mediaQueue.remove(fromIndex));
|
||||
|
||||
// Index update.
|
||||
if (fromIndex == currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(toIndex);
|
||||
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(newIndex);
|
||||
} else if (fromIndex < currentItemIndex && newIndex >= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
||||
} else if (fromIndex > currentItemIndex && newIndex <= currentItemIndex) {
|
||||
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
||||
}
|
||||
|
||||
@ -257,7 +215,6 @@ import java.util.Map;
|
||||
public void release() {
|
||||
currentItemIndex = C.INDEX_UNSET;
|
||||
mediaQueue.clear();
|
||||
concatenatingMediaSource.clear();
|
||||
castPlayer.setSessionAvailabilityListener(null);
|
||||
castPlayer.release();
|
||||
localPlayerView.setPlayer(null);
|
||||
@ -267,7 +224,7 @@ import java.util.Map;
|
||||
// Player.EventListener implementation.
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@ -277,12 +234,13 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
||||
public void onTimelineChanged(@NonNull Timeline timeline, @TimelineChangeReason int reason) {
|
||||
updateCurrentItemIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
public void onTracksChanged(
|
||||
@NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) {
|
||||
if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) {
|
||||
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
|
||||
trackSelector.getCurrentMappedTrackInfo();
|
||||
@ -360,35 +318,26 @@ import java.util.Map;
|
||||
this.currentPlayer = currentPlayer;
|
||||
|
||||
// Media queue management.
|
||||
if (currentPlayer == exoPlayer) {
|
||||
exoPlayer.prepare(concatenatingMediaSource);
|
||||
}
|
||||
|
||||
// Playback transition.
|
||||
if (windowIndex != C.INDEX_UNSET) {
|
||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
||||
}
|
||||
currentPlayer.setMediaItems(mediaQueue, windowIndex, playbackPositionMs);
|
||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||
currentPlayer.prepare();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback of the item at the given position.
|
||||
* Starts playback of the item at the given index.
|
||||
*
|
||||
* @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) {
|
||||
private void setCurrentItem(int itemIndex) {
|
||||
maybeSetCurrentItemAndNotify(itemIndex);
|
||||
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
|
||||
}
|
||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||
if (currentPlayer.getCurrentTimeline().getWindowCount() != mediaQueue.size()) {
|
||||
// This only happens with the cast player. The receiver app in the cast device clears the
|
||||
// timeline when the last item of the timeline has been played to end.
|
||||
currentPlayer.setMediaItems(mediaQueue, itemIndex, C.TIME_UNSET);
|
||||
} else {
|
||||
currentPlayer.seekTo(itemIndex, positionMs);
|
||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||
currentPlayer.seekTo(itemIndex, C.TIME_UNSET);
|
||||
}
|
||||
currentPlayer.setPlayWhenReady(true);
|
||||
}
|
||||
|
||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
||||
@ -398,62 +347,4 @@ import java.util.Map;
|
||||
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(MediaItem item) {
|
||||
Uri uri = item.uri;
|
||||
String mimeType = item.mimeType;
|
||||
if (mimeType == null) {
|
||||
throw new IllegalArgumentException("mimeType is required");
|
||||
}
|
||||
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager =
|
||||
DrmSessionManager.getDummyDrmSessionManager();
|
||||
MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration;
|
||||
if (drmConfiguration != null && Util.SDK_INT >= 18) {
|
||||
String licenseServerUrl =
|
||||
drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : "";
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY);
|
||||
for (Map.Entry<String, String> requestHeader : drmConfiguration.requestHeaders.entrySet()) {
|
||||
drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue());
|
||||
}
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setMultiSession(/* multiSession= */ true)
|
||||
.setUuidAndExoMediaDrmProvider(
|
||||
drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.build(drmCallback);
|
||||
}
|
||||
|
||||
MediaSource createdMediaSource;
|
||||
switch (mimeType) {
|
||||
case DemoUtil.MIME_TYPE_SS:
|
||||
createdMediaSource =
|
||||
new SsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
case DemoUtil.MIME_TYPE_DASH:
|
||||
createdMediaSource =
|
||||
new DashMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
case DemoUtil.MIME_TYPE_HLS:
|
||||
createdMediaSource =
|
||||
new HlsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||
createdMediaSource =
|
||||
new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("mimeType is unsupported: " + mimeType);
|
||||
}
|
||||
return createdMediaSource;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.castdemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -49,5 +49,5 @@ dependencies {
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
}
|
||||
|
@ -32,4 +32,3 @@ void main() {
|
||||
gl_FragColor = videoColor * (1.0 - overlayColor.a)
|
||||
+ overlayColor * overlayColor.a;
|
||||
}
|
||||
|
||||
|
@ -11,11 +11,10 @@
|
||||
// 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.
|
||||
attribute vec4 a_position;
|
||||
attribute vec3 a_texcoord;
|
||||
attribute vec2 a_position;
|
||||
attribute vec2 a_texcoord;
|
||||
varying vec2 v_texcoord;
|
||||
void main() {
|
||||
gl_Position = a_position;
|
||||
v_texcoord = a_texcoord.xy;
|
||||
gl_Position = vec4(a_position.x, a_position.y, 0, 1);
|
||||
v_texcoord = a_texcoord;
|
||||
}
|
||||
|
||||
|
@ -88,18 +88,9 @@ import javax.microedition.khronos.opengles.GL10;
|
||||
GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program);
|
||||
for (GlUtil.Attribute attribute : attributes) {
|
||||
if (attribute.name.equals("a_position")) {
|
||||
attribute.setBuffer(
|
||||
new float[] {
|
||||
-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
|
||||
1.0f, 0.0f, 1.0f,
|
||||
},
|
||||
4);
|
||||
attribute.setBuffer(new float[] {-1, -1, 1, -1, -1, 1, 1, 1}, 2);
|
||||
} else if (attribute.name.equals("a_texcoord")) {
|
||||
attribute.setBuffer(
|
||||
new float[] {
|
||||
0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,
|
||||
},
|
||||
3);
|
||||
attribute.setBuffer(new float[] {0, 1, 1, 1, 0, 0, 1, 0}, 2);
|
||||
}
|
||||
}
|
||||
this.attributes = attributes;
|
||||
|
@ -24,11 +24,11 @@ import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
@ -51,6 +51,8 @@ import java.util.UUID;
|
||||
*/
|
||||
public final class MainActivity extends Activity {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
private static final String DEFAULT_MEDIA_URI =
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv";
|
||||
|
||||
@ -137,13 +139,12 @@ public final class MainActivity extends Activity {
|
||||
ACTION_VIEW.equals(action)
|
||||
? Assertions.checkNotNull(intent.getData())
|
||||
: Uri.parse(DEFAULT_MEDIA_URI);
|
||||
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||
DrmSessionManager drmSessionManager;
|
||||
if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) {
|
||||
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
|
||||
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
|
||||
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
|
||||
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
|
||||
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||
drmSessionManager =
|
||||
@ -154,29 +155,28 @@ public final class MainActivity extends Activity {
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
}
|
||||
|
||||
DataSource.Factory dataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
this, Util.getUserAgent(this, getString(R.string.application_name)));
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
|
||||
MediaSource mediaSource;
|
||||
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
|
||||
if (type == C.TYPE_DASH) {
|
||||
mediaSource =
|
||||
new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
|
||||
player.setRepeatMode(Player.REPEAT_MODE_ALL);
|
||||
player.prepare(mediaSource);
|
||||
player.setPlayWhenReady(true);
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
player.play();
|
||||
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
|
||||
Assertions.checkNotNull(this.videoProcessingGLSurfaceView);
|
||||
videoProcessingGLSurfaceView.setVideoComponent(
|
||||
|
@ -23,6 +23,7 @@ import android.opengl.GLES20;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Handler;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
@ -83,6 +84,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
||||
* created, if supported by the device.
|
||||
* @param videoProcessor Processor that draws to the view.
|
||||
*/
|
||||
@SuppressWarnings("InlinedApi")
|
||||
public VideoProcessingGLSurfaceView(
|
||||
Context context, boolean requireSecureContext, VideoProcessor videoProcessor) {
|
||||
super(context);
|
||||
@ -282,7 +284,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
||||
public void onVideoFrameAboutToBeRendered(
|
||||
long presentationTimeUs,
|
||||
long releaseTimeNs,
|
||||
Format format,
|
||||
@NonNull Format format,
|
||||
@Nullable MediaFormat mediaFormat) {
|
||||
sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs);
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.gldemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -27,4 +27,3 @@
|
||||
app:surface_type="none"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -27,6 +27,7 @@ android {
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -49,34 +50,46 @@ android {
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
|
||||
}
|
||||
|
||||
flavorDimensions "extensions"
|
||||
flavorDimensions "decoderExtensions"
|
||||
|
||||
productFlavors {
|
||||
noExtensions {
|
||||
dimension "extensions"
|
||||
noDecoderExtensions {
|
||||
dimension "decoderExtensions"
|
||||
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "false"
|
||||
}
|
||||
withExtensions {
|
||||
dimension "extensions"
|
||||
withDecoderExtensions {
|
||||
dimension "decoderExtensions"
|
||||
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation ('com.google.guava:guava:' + guavaVersion) {
|
||||
exclude group: 'com.google.code.findbugs', module: 'jsr305'
|
||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
||||
}
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-opus')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
|
||||
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
|
||||
implementation project(modulePrefix + 'extension-cronet')
|
||||
implementation project(modulePrefix + 'extension-ima')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-av1')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-ffmpeg')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-flac')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-opus')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-vp9')
|
||||
withDecoderExtensionsImplementation project(modulePrefix + 'extension-rtmp')
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||
|
@ -1,7 +1,2 @@
|
||||
# Proguard rules specific to the main demo app.
|
||||
|
||||
# Constructor accessed via reflection in PlayerActivity
|
||||
-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
|
||||
-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
|
||||
<init>(android.content.Context, android.net.Uri);
|
||||
}
|
||||
|
@ -35,8 +35,8 @@
|
||||
android:largeHeap="true"
|
||||
android:allowBackup="false"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:name="com.google.android.exoplayer2.demo.DemoApplication"
|
||||
tools:ignore="UnusedAttribute">
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
tools:targetApi="29">
|
||||
|
||||
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
|
@ -31,73 +31,73 @@
|
||||
"name": "SW secure crypto (L3)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "SW secure decode",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_DECODE&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_DECODE&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HW secure crypto",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_CRYPTO&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_CRYPTO&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HW secure decode",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_DECODE&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_DECODE&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HW secure all (L1)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "30s license (fails at ~30s)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_CAN_RENEW_FALSE_LICENSE_30S_PLAYBACK_30S&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_CAN_RENEW_FALSE_LICENSE_30S_PLAYBACK_30S&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP not required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NONE&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NONE&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 1.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V1&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 2.0 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 2.1 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_1&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_1&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP 2.2 required",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_2&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_V2_2&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "HDCP no digital output",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NO_DIGITAL_OUTPUT&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO_HDCP_NO_DIGITAL_OUTPUT&provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -116,37 +116,32 @@
|
||||
"name": "Secure (cenc)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure UHD (cenc)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure (cbc1)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure UHD (cbc1)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure (cbcs)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure UHD (cbcs)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure -> Clear -> Secure (cenc)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||
"drm_session_for_clear_content": true
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -165,25 +160,25 @@
|
||||
"name": "Secure (full-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure UHD (full-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure (sub-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure UHD (sub-sample)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -202,13 +197,13 @@
|
||||
"name": "Secure",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure UHD",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -223,13 +218,13 @@
|
||||
"name": "Secure L3",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Secure L1",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -330,6 +325,10 @@
|
||||
{
|
||||
"name": "Big Buck Bunny 480p video (MP4,AV1)",
|
||||
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
|
||||
},
|
||||
{
|
||||
"name": "One hour frame counter (MP4)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -370,7 +369,7 @@
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||
@ -378,12 +377,29 @@
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Manual ad insertion",
|
||||
"playlist": [
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"clip_end_position_ms": 10000
|
||||
},
|
||||
{
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||
"clip_end_position_ms": 5000
|
||||
},
|
||||
{
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"clip_start_position_ms": 10000
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -484,26 +500,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "360",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Congo (360 top-bottom stereo)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
},
|
||||
{
|
||||
"name": "Sphericalv2 (180 top-bottom stereo)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
},
|
||||
{
|
||||
"name": "Iceland (360 top-bottom stereo ts)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
|
||||
"spherical_stereo_mode": "top_bottom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Subtitles",
|
||||
"samples": [
|
||||
@ -514,12 +510,48 @@
|
||||
"subtitle_mime_type": "application/ttml+xml",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "WebVTT line positioning",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/numeric-lines.vtt",
|
||||
"subtitle_mime_type": "text/vtt",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "SSA/ASS position & alignment",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass",
|
||||
"subtitle_mime_type": "text/x-ssa",
|
||||
"subtitle_language": "en"
|
||||
},
|
||||
{
|
||||
"name": "MPEG-4 Timed Text (tx3g, mov_text)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "60fps",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Big Buck Bunny (DASH,H264,1080p,Clear)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-1080/manifest.mpd"
|
||||
},
|
||||
{
|
||||
"name": "Big Buck Bunny (DASH,H264,4K,Clear)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-2160/manifest.mpd"
|
||||
},
|
||||
{
|
||||
"name": "Big Buck Bunny (DASH,H264,1080p,Widevine)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-1080/manifest.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "Big Buck Bunny (DASH,H264,4K,Widevine)",
|
||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-2160/manifest.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,185 +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.demo;
|
||||
|
||||
import android.app.Application;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
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.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
||||
*/
|
||||
public class DemoApplication extends Application {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
|
||||
private static final String TAG = "DemoApplication";
|
||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||
|
||||
protected String userAgent;
|
||||
|
||||
private DatabaseProvider databaseProvider;
|
||||
private File downloadDirectory;
|
||||
private Cache downloadCache;
|
||||
private DownloadManager downloadManager;
|
||||
private DownloadTracker downloadTracker;
|
||||
private DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
|
||||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public DataSource.Factory buildDataSourceFactory() {
|
||||
DefaultDataSourceFactory upstreamFactory =
|
||||
new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
|
||||
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
|
||||
}
|
||||
|
||||
/** Returns a {@link HttpDataSource.Factory}. */
|
||||
public HttpDataSource.Factory buildHttpDataSourceFactory() {
|
||||
return new DefaultHttpDataSourceFactory(userAgent);
|
||||
}
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
public boolean useExtensionRenderers() {
|
||||
return "withExtensions".equals(BuildConfig.FLAVOR);
|
||||
}
|
||||
|
||||
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
|
||||
@DefaultRenderersFactory.ExtensionRendererMode
|
||||
int extensionRendererMode =
|
||||
useExtensionRenderers()
|
||||
? (preferExtensionRenderer
|
||||
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||
return new DefaultRenderersFactory(/* context= */ this)
|
||||
.setExtensionRendererMode(extensionRendererMode);
|
||||
}
|
||||
|
||||
public DownloadNotificationHelper getDownloadNotificationHelper() {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(this, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
public DownloadManager getDownloadManager() {
|
||||
initDownloadManager();
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
public DownloadTracker getDownloadTracker() {
|
||||
initDownloadManager();
|
||||
return downloadTracker;
|
||||
}
|
||||
|
||||
protected synchronized Cache getDownloadCache() {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
private synchronized void initDownloadManager() {
|
||||
if (downloadManager == null) {
|
||||
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
|
||||
upgradeActionFile(
|
||||
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||
upgradeActionFile(
|
||||
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
|
||||
DownloaderConstructorHelper downloaderConstructorHelper =
|
||||
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
|
||||
downloadTracker =
|
||||
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
private void upgradeActionFile(
|
||||
String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
|
||||
try {
|
||||
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||
new File(getDownloadDirectory(), fileName),
|
||||
/* downloadIdProvider= */ null,
|
||||
downloadIndex,
|
||||
/* deleteOnFailure= */ true,
|
||||
addNewDownloadsAsCompleted);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseProvider getDatabaseProvider() {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new ExoDatabaseProvider(this);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private File getDownloadDirectory() {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = getExternalFilesDir(null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
|
||||
protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
|
||||
DataSource.Factory upstreamFactory, Cache cache) {
|
||||
return new CacheDataSourceFactory(
|
||||
cache,
|
||||
upstreamFactory,
|
||||
new FileDataSource.Factory(),
|
||||
/* cacheWriteDataSinkFactory= */ null,
|
||||
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
|
||||
/* eventListener= */ null);
|
||||
}
|
||||
}
|
@ -15,10 +15,12 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.demo.DemoApplication.DOWNLOAD_NOTIFICATION_CHANNEL_ID;
|
||||
import static com.google.android.exoplayer2.demo.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.offline.Download;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
@ -44,13 +46,13 @@ public class DemoDownloadService extends DownloadService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
protected DownloadManager getDownloadManager() {
|
||||
// This will only happen once, because getDownloadManager is guaranteed to be called only once
|
||||
// in the life cycle of the process.
|
||||
DemoApplication application = (DemoApplication) getApplication();
|
||||
DownloadManager downloadManager = application.getDownloadManager();
|
||||
DownloadManager downloadManager = DemoUtil.getDownloadManager(/* context= */ this);
|
||||
DownloadNotificationHelper downloadNotificationHelper =
|
||||
application.getDownloadNotificationHelper();
|
||||
DemoUtil.getDownloadNotificationHelper(/* context= */ this);
|
||||
downloadManager.addListener(
|
||||
new TerminalStateNotificationHelper(
|
||||
this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1));
|
||||
@ -63,11 +65,15 @@ public class DemoDownloadService extends DownloadService {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Notification getForegroundNotification(List<Download> downloads) {
|
||||
return ((DemoApplication) getApplication())
|
||||
.getDownloadNotificationHelper()
|
||||
@NonNull
|
||||
protected Notification getForegroundNotification(@NonNull List<Download> downloads) {
|
||||
return DemoUtil.getDownloadNotificationHelper(/* context= */ this)
|
||||
.buildProgressNotification(
|
||||
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
|
||||
/* context= */ this,
|
||||
R.drawable.ic_download,
|
||||
/* contentIntent= */ null,
|
||||
/* message= */ null,
|
||||
downloads);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,17 +97,20 @@ public class DemoDownloadService extends DownloadService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadChanged(DownloadManager manager, Download download) {
|
||||
public void onDownloadChanged(
|
||||
DownloadManager downloadManager, Download download, @Nullable Exception finalException) {
|
||||
Notification notification;
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadCompletedNotification(
|
||||
context,
|
||||
R.drawable.ic_download_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
notification =
|
||||
notificationHelper.buildDownloadFailedNotification(
|
||||
context,
|
||||
R.drawable.ic_download_done,
|
||||
/* contentIntent= */ null,
|
||||
Util.fromUtf8Bytes(download.request.data));
|
||||
|
@ -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.demo;
|
||||
|
||||
import android.content.Context;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||
import com.google.android.exoplayer2.ext.cronet.CronetDataSourceFactory;
|
||||
import com.google.android.exoplayer2.ext.cronet.CronetEngineWrapper;
|
||||
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Utility methods for the demo app. */
|
||||
public final class DemoUtil {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
|
||||
private static final String TAG = "DemoUtil";
|
||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||
|
||||
private static DataSource.@MonotonicNonNull Factory dataSourceFactory;
|
||||
private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory;
|
||||
private static @MonotonicNonNull DatabaseProvider databaseProvider;
|
||||
private static @MonotonicNonNull File downloadDirectory;
|
||||
private static @MonotonicNonNull Cache downloadCache;
|
||||
private static @MonotonicNonNull DownloadManager downloadManager;
|
||||
private static @MonotonicNonNull DownloadTracker downloadTracker;
|
||||
private static @MonotonicNonNull DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
public static boolean useExtensionRenderers() {
|
||||
return BuildConfig.USE_DECODER_EXTENSIONS;
|
||||
}
|
||||
|
||||
public static RenderersFactory buildRenderersFactory(
|
||||
Context context, boolean preferExtensionRenderer) {
|
||||
@DefaultRenderersFactory.ExtensionRendererMode
|
||||
int extensionRendererMode =
|
||||
useExtensionRenderers()
|
||||
? (preferExtensionRenderer
|
||||
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||
return new DefaultRenderersFactory(context.getApplicationContext())
|
||||
.setExtensionRendererMode(extensionRendererMode);
|
||||
}
|
||||
|
||||
public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) {
|
||||
if (httpDataSourceFactory == null) {
|
||||
context = context.getApplicationContext();
|
||||
CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper(context);
|
||||
httpDataSourceFactory =
|
||||
new CronetDataSourceFactory(cronetEngineWrapper, Executors.newSingleThreadExecutor());
|
||||
}
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
|
||||
if (dataSourceFactory == null) {
|
||||
context = context.getApplicationContext();
|
||||
DefaultDataSourceFactory upstreamFactory =
|
||||
new DefaultDataSourceFactory(context, getHttpDataSourceFactory(context));
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
|
||||
Context context) {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
public static synchronized DownloadManager getDownloadManager(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
public static synchronized DownloadTracker getDownloadTracker(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadTracker;
|
||||
}
|
||||
|
||||
private static synchronized Cache getDownloadCache(Context context) {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory =
|
||||
new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(
|
||||
downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context));
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
private static synchronized void ensureDownloadManagerInitialized(Context context) {
|
||||
if (downloadManager == null) {
|
||||
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider(context));
|
||||
upgradeActionFile(
|
||||
context, DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||
upgradeActionFile(
|
||||
context,
|
||||
DOWNLOAD_TRACKER_ACTION_FILE,
|
||||
downloadIndex,
|
||||
/* addNewDownloadsAsCompleted= */ true);
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
context,
|
||||
getDatabaseProvider(context),
|
||||
getDownloadCache(context),
|
||||
getHttpDataSourceFactory(context),
|
||||
Executors.newFixedThreadPool(/* nThreads= */ 6));
|
||||
downloadTracker =
|
||||
new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void upgradeActionFile(
|
||||
Context context,
|
||||
String fileName,
|
||||
DefaultDownloadIndex downloadIndex,
|
||||
boolean addNewDownloadsAsCompleted) {
|
||||
try {
|
||||
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||
new File(getDownloadDirectory(context), fileName),
|
||||
/* downloadIdProvider= */ null,
|
||||
downloadIndex,
|
||||
/* deleteOnFailure= */ true,
|
||||
addNewDownloadsAsCompleted);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new ExoDatabaseProvider(context);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private static synchronized File getDownloadDirectory(Context context) {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(/* type= */ null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
|
||||
private static CacheDataSource.Factory buildReadOnlyCacheDataSource(
|
||||
DataSource.Factory upstreamFactory, Cache cache) {
|
||||
return new CacheDataSource.Factory()
|
||||
.setCache(cache)
|
||||
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||
.setCacheWriteDataSinkFactory(null)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
|
||||
}
|
||||
|
||||
private DemoUtil() {}
|
||||
}
|
@ -15,24 +15,38 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||
import com.google.android.exoplayer2.drm.OfflineLicenseHelper;
|
||||
import com.google.android.exoplayer2.offline.Download;
|
||||
import com.google.android.exoplayer2.offline.DownloadCursor;
|
||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||
import com.google.android.exoplayer2.offline.DownloadHelper.LiveContentUnsupportedException;
|
||||
import com.google.android.exoplayer2.offline.DownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
@ -52,7 +66,7 @@ public class DownloadTracker {
|
||||
private static final String TAG = "DownloadTracker";
|
||||
|
||||
private final Context context;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final HttpDataSource.Factory httpDataSourceFactory;
|
||||
private final CopyOnWriteArraySet<Listener> listeners;
|
||||
private final HashMap<Uri, Download> downloads;
|
||||
private final DownloadIndex downloadIndex;
|
||||
@ -61,9 +75,11 @@ public class DownloadTracker {
|
||||
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
|
||||
|
||||
public DownloadTracker(
|
||||
Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
|
||||
Context context,
|
||||
HttpDataSource.Factory httpDataSourceFactory,
|
||||
DownloadManager downloadManager) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.httpDataSourceFactory = httpDataSourceFactory;
|
||||
listeners = new CopyOnWriteArraySet<>();
|
||||
downloads = new HashMap<>();
|
||||
downloadIndex = downloadManager.getDownloadIndex();
|
||||
@ -73,6 +89,7 @@ public class DownloadTracker {
|
||||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
checkNotNull(listener);
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@ -80,23 +97,20 @@ public class DownloadTracker {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public boolean isDownloaded(Uri uri) {
|
||||
Download download = downloads.get(uri);
|
||||
public boolean isDownloaded(MediaItem mediaItem) {
|
||||
Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
||||
return download != null && download.state != Download.STATE_FAILED;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public DownloadRequest getDownloadRequest(Uri uri) {
|
||||
Download download = downloads.get(uri);
|
||||
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
|
||||
}
|
||||
|
||||
public void toggleDownload(
|
||||
FragmentManager fragmentManager,
|
||||
String name,
|
||||
Uri uri,
|
||||
String extension,
|
||||
RenderersFactory renderersFactory) {
|
||||
Download download = downloads.get(uri);
|
||||
FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) {
|
||||
Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
||||
if (download != null) {
|
||||
DownloadService.sendRemoveDownload(
|
||||
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
|
||||
@ -106,7 +120,10 @@ public class DownloadTracker {
|
||||
}
|
||||
startDownloadDialogHelper =
|
||||
new StartDownloadDialogHelper(
|
||||
fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
|
||||
fragmentManager,
|
||||
DownloadHelper.forMediaItem(
|
||||
context, mediaItem, renderersFactory, httpDataSourceFactory),
|
||||
mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,27 +138,13 @@ public class DownloadTracker {
|
||||
}
|
||||
}
|
||||
|
||||
private DownloadHelper getDownloadHelper(
|
||||
Uri uri, String extension, RenderersFactory renderersFactory) {
|
||||
int type = Util.inferContentType(uri, extension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_SS:
|
||||
return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_HLS:
|
||||
return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
|
||||
case C.TYPE_OTHER:
|
||||
return DownloadHelper.forProgressive(context, uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadManagerListener implements DownloadManager.Listener {
|
||||
|
||||
@Override
|
||||
public void onDownloadChanged(DownloadManager downloadManager, Download download) {
|
||||
public void onDownloadChanged(
|
||||
@NonNull DownloadManager downloadManager,
|
||||
@NonNull Download download,
|
||||
@Nullable Exception finalException) {
|
||||
downloads.put(download.request.uri, download);
|
||||
for (Listener listener : listeners) {
|
||||
listener.onDownloadsChanged();
|
||||
@ -149,7 +152,8 @@ public class DownloadTracker {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadRemoved(DownloadManager downloadManager, Download download) {
|
||||
public void onDownloadRemoved(
|
||||
@NonNull DownloadManager downloadManager, @NonNull Download download) {
|
||||
downloads.remove(download.request.uri);
|
||||
for (Listener listener : listeners) {
|
||||
listener.onDownloadsChanged();
|
||||
@ -164,16 +168,18 @@ public class DownloadTracker {
|
||||
|
||||
private final FragmentManager fragmentManager;
|
||||
private final DownloadHelper downloadHelper;
|
||||
private final String name;
|
||||
private final MediaItem mediaItem;
|
||||
|
||||
private TrackSelectionDialog trackSelectionDialog;
|
||||
private MappedTrackInfo mappedTrackInfo;
|
||||
private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask;
|
||||
@Nullable private byte[] keySetId;
|
||||
|
||||
public StartDownloadDialogHelper(
|
||||
FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
|
||||
FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) {
|
||||
this.fragmentManager = fragmentManager;
|
||||
this.downloadHelper = downloadHelper;
|
||||
this.name = name;
|
||||
this.mediaItem = mediaItem;
|
||||
downloadHelper.prepare(this);
|
||||
}
|
||||
|
||||
@ -182,46 +188,57 @@ public class DownloadTracker {
|
||||
if (trackSelectionDialog != null) {
|
||||
trackSelectionDialog.dismiss();
|
||||
}
|
||||
if (widevineOfflineLicenseFetchTask != null) {
|
||||
widevineOfflineLicenseFetchTask.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadHelper.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onPrepared(DownloadHelper helper) {
|
||||
if (helper.getPeriodCount() == 0) {
|
||||
Log.d(TAG, "No periods found. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
public void onPrepared(@NonNull DownloadHelper helper) {
|
||||
@Nullable Format format = getFirstFormatWithDrmInitData(helper);
|
||||
if (format == null) {
|
||||
onDownloadPrepared(helper);
|
||||
return;
|
||||
}
|
||||
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
|
||||
Log.d(TAG, "No dialog content. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
|
||||
// The content is DRM protected. We need to acquire an offline license.
|
||||
if (Util.SDK_INT < 18) {
|
||||
Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18");
|
||||
return;
|
||||
}
|
||||
trackSelectionDialog =
|
||||
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
|
||||
/* titleId= */ R.string.exo_download_description,
|
||||
mappedTrackInfo,
|
||||
trackSelectorParameters,
|
||||
/* allowAdaptiveSelections =*/ false,
|
||||
/* allowMultipleOverrides= */ true,
|
||||
/* onClickListener= */ this,
|
||||
/* onDismissListener= */ this);
|
||||
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
|
||||
// TODO(internal b/163107948): Support cases where DrmInitData are not in the manifest.
|
||||
if (!hasSchemaData(format.drmInitData)) {
|
||||
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(
|
||||
TAG,
|
||||
"Downloading content where DRM scheme data is not located in the manifest is not"
|
||||
+ " supported");
|
||||
return;
|
||||
}
|
||||
widevineOfflineLicenseFetchTask =
|
||||
new WidevineOfflineLicenseFetchTask(
|
||||
format,
|
||||
mediaItem.playbackProperties.drmConfiguration.licenseUri,
|
||||
httpDataSourceFactory,
|
||||
/* dialogHelper= */ this,
|
||||
helper);
|
||||
widevineOfflineLicenseFetchTask.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareError(DownloadHelper helper, IOException e) {
|
||||
Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
|
||||
Log.e(
|
||||
TAG,
|
||||
e instanceof DownloadHelper.LiveContentUnsupportedException
|
||||
? "Downloading live content unsupported"
|
||||
: "Failed to start download",
|
||||
e);
|
||||
public void onPrepareError(@NonNull DownloadHelper helper, @NonNull IOException e) {
|
||||
boolean isLiveContent = e instanceof LiveContentUnsupportedException;
|
||||
int toastStringId =
|
||||
isLiveContent ? R.string.download_live_unsupported : R.string.download_start_error;
|
||||
String logMessage =
|
||||
isLiveContent ? "Downloading live content unsupported" : "Failed to start download";
|
||||
Toast.makeText(context, toastStringId, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, logMessage, e);
|
||||
}
|
||||
|
||||
// DialogInterface.OnClickListener implementation.
|
||||
@ -258,6 +275,83 @@ public class DownloadTracker {
|
||||
|
||||
// Internal methods.
|
||||
|
||||
/**
|
||||
* Returns the first {@link Format} with a non-null {@link Format#drmInitData} found in the
|
||||
* content's tracks, or null if none is found.
|
||||
*/
|
||||
@Nullable
|
||||
private Format getFirstFormatWithDrmInitData(DownloadHelper helper) {
|
||||
for (int periodIndex = 0; periodIndex < helper.getPeriodCount(); periodIndex++) {
|
||||
MappedTrackInfo mappedTrackInfo = helper.getMappedTrackInfo(periodIndex);
|
||||
for (int rendererIndex = 0;
|
||||
rendererIndex < mappedTrackInfo.getRendererCount();
|
||||
rendererIndex++) {
|
||||
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
|
||||
for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) {
|
||||
TrackGroup trackGroup = trackGroups.get(trackGroupIndex);
|
||||
for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) {
|
||||
Format format = trackGroup.getFormat(formatIndex);
|
||||
if (format.drmInitData != null) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void onOfflineLicenseFetched(DownloadHelper helper, byte[] keySetId) {
|
||||
this.keySetId = keySetId;
|
||||
onDownloadPrepared(helper);
|
||||
}
|
||||
|
||||
private void onOfflineLicenseFetchedError(DrmSession.DrmSessionException e) {
|
||||
Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Failed to fetch offline DRM license", e);
|
||||
}
|
||||
|
||||
private void onDownloadPrepared(DownloadHelper helper) {
|
||||
if (helper.getPeriodCount() == 0) {
|
||||
Log.d(TAG, "No periods found. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
return;
|
||||
}
|
||||
|
||||
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
|
||||
Log.d(TAG, "No dialog content. Downloading entire stream.");
|
||||
startDownload();
|
||||
downloadHelper.release();
|
||||
return;
|
||||
}
|
||||
trackSelectionDialog =
|
||||
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
|
||||
/* titleId= */ R.string.exo_download_description,
|
||||
mappedTrackInfo,
|
||||
trackSelectorParameters,
|
||||
/* allowAdaptiveSelections =*/ false,
|
||||
/* allowMultipleOverrides= */ true,
|
||||
/* onClickListener= */ this,
|
||||
/* onDismissListener= */ this);
|
||||
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has
|
||||
* non-null {@link DrmInitData.SchemeData#data}.
|
||||
*/
|
||||
private boolean hasSchemaData(DrmInitData drmInitData) {
|
||||
for (int i = 0; i < drmInitData.schemeDataCount; i++) {
|
||||
if (drmInitData.get(i).hasData()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void startDownload() {
|
||||
startDownload(buildDownloadRequest());
|
||||
}
|
||||
@ -268,7 +362,62 @@ public class DownloadTracker {
|
||||
}
|
||||
|
||||
private DownloadRequest buildDownloadRequest() {
|
||||
return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
|
||||
return downloadHelper
|
||||
.getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title)))
|
||||
.copyWithKeySetId(keySetId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Downloads a Widevine offline license in a background thread. */
|
||||
@RequiresApi(18)
|
||||
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
private final Format format;
|
||||
private final Uri licenseUri;
|
||||
private final HttpDataSource.Factory httpDataSourceFactory;
|
||||
private final StartDownloadDialogHelper dialogHelper;
|
||||
private final DownloadHelper downloadHelper;
|
||||
|
||||
@Nullable private byte[] keySetId;
|
||||
@Nullable private DrmSession.DrmSessionException drmSessionException;
|
||||
|
||||
public WidevineOfflineLicenseFetchTask(
|
||||
Format format,
|
||||
Uri licenseUri,
|
||||
HttpDataSource.Factory httpDataSourceFactory,
|
||||
StartDownloadDialogHelper dialogHelper,
|
||||
DownloadHelper downloadHelper) {
|
||||
this.format = format;
|
||||
this.licenseUri = licenseUri;
|
||||
this.httpDataSourceFactory = httpDataSourceFactory;
|
||||
this.dialogHelper = dialogHelper;
|
||||
this.downloadHelper = downloadHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
OfflineLicenseHelper offlineLicenseHelper =
|
||||
OfflineLicenseHelper.newWidevineInstance(
|
||||
licenseUri.toString(),
|
||||
httpDataSourceFactory,
|
||||
new DrmSessionEventListener.EventDispatcher());
|
||||
try {
|
||||
keySetId = offlineLicenseHelper.downloadLicense(format);
|
||||
} catch (DrmSession.DrmSessionException e) {
|
||||
drmSessionException = e;
|
||||
} finally {
|
||||
offlineLicenseHelper.release();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
if (drmSessionException != null) {
|
||||
dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
|
||||
} else {
|
||||
dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Util to read from and populate an intent. */
|
||||
public class IntentUtil {
|
||||
|
||||
// Actions.
|
||||
|
||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||
public static final String ACTION_VIEW_LIST =
|
||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||
|
||||
// Activity extras.
|
||||
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||
|
||||
// Media item configuration extras.
|
||||
|
||||
public static final String URI_EXTRA = "uri";
|
||||
public static final String MIME_TYPE_EXTRA = "mime_type";
|
||||
public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms";
|
||||
public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms";
|
||||
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
|
||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
public static final String DRM_LICENSE_URI_EXTRA = "drm_license_uri";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||
public static final String DRM_SESSION_FOR_CLEAR_CONTENT = "drm_session_for_clear_content";
|
||||
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||
public static final String DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA = "drm_force_default_license_uri";
|
||||
|
||||
public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
|
||||
public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
|
||||
public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
|
||||
|
||||
/** Creates a list of {@link MediaItem media items} from an {@link Intent}. */
|
||||
public static List<MediaItem> createMediaItemsFromIntent(Intent intent) {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
if (ACTION_VIEW_LIST.equals(intent.getAction())) {
|
||||
int index = 0;
|
||||
while (intent.hasExtra(URI_EXTRA + "_" + index)) {
|
||||
Uri uri = Uri.parse(intent.getStringExtra(URI_EXTRA + "_" + index));
|
||||
mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + index));
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
Uri uri = intent.getData();
|
||||
mediaItems.add(createMediaItemFromIntent(uri, intent, /* extrasKeySuffix= */ ""));
|
||||
}
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
/** Populates the intent with the given list of {@link MediaItem media items}. */
|
||||
public static void addToIntent(List<MediaItem> mediaItems, Intent intent) {
|
||||
Assertions.checkArgument(!mediaItems.isEmpty());
|
||||
if (mediaItems.size() == 1) {
|
||||
MediaItem mediaItem = mediaItems.get(0);
|
||||
MediaItem.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties);
|
||||
intent.setAction(ACTION_VIEW).setData(mediaItem.playbackProperties.uri);
|
||||
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "");
|
||||
addClippingPropertiesToIntent(
|
||||
mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ "");
|
||||
} else {
|
||||
intent.setAction(ACTION_VIEW_LIST);
|
||||
for (int i = 0; i < mediaItems.size(); i++) {
|
||||
MediaItem mediaItem = mediaItems.get(i);
|
||||
MediaItem.PlaybackProperties playbackProperties =
|
||||
checkNotNull(mediaItem.playbackProperties);
|
||||
intent.putExtra(URI_EXTRA + ("_" + i), playbackProperties.uri.toString());
|
||||
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
addClippingPropertiesToIntent(
|
||||
mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaItem createMediaItemFromIntent(
|
||||
Uri uri, Intent intent, String extrasKeySuffix) {
|
||||
@Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
|
||||
MediaItem.Builder builder =
|
||||
new MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setMimeType(mimeType)
|
||||
.setAdTagUri(intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix))
|
||||
.setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix))
|
||||
.setClipStartPositionMs(
|
||||
intent.getLongExtra(CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, 0))
|
||||
.setClipEndPositionMs(
|
||||
intent.getLongExtra(
|
||||
CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, C.TIME_END_OF_SOURCE));
|
||||
|
||||
return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build();
|
||||
}
|
||||
|
||||
private static List<MediaItem.Subtitle> createSubtitlesFromIntent(
|
||||
Intent intent, String extrasKeySuffix) {
|
||||
if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.singletonList(
|
||||
new MediaItem.Subtitle(
|
||||
Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)),
|
||||
checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix)),
|
||||
intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix),
|
||||
C.SELECTION_FLAG_DEFAULT));
|
||||
}
|
||||
|
||||
private static MediaItem.Builder populateDrmPropertiesFromIntent(
|
||||
MediaItem.Builder builder, Intent intent, String extrasKeySuffix) {
|
||||
String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
|
||||
@Nullable String drmSchemeExtra = intent.getStringExtra(schemeKey);
|
||||
if (drmSchemeExtra == null) {
|
||||
return builder;
|
||||
}
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
@Nullable
|
||||
String[] keyRequestPropertiesArray =
|
||||
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
|
||||
if (keyRequestPropertiesArray != null) {
|
||||
for (int i = 0; i < keyRequestPropertiesArray.length; i += 2) {
|
||||
headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
builder
|
||||
.setDrmUuid(Util.getDrmUuid(Util.castNonNull(drmSchemeExtra)))
|
||||
.setDrmLicenseUri(intent.getStringExtra(DRM_LICENSE_URI_EXTRA + extrasKeySuffix))
|
||||
.setDrmMultiSession(
|
||||
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false))
|
||||
.setDrmForceDefaultLicenseUri(
|
||||
intent.getBooleanExtra(DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false))
|
||||
.setDrmLicenseRequestHeaders(headers);
|
||||
if (intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false)) {
|
||||
builder.setDrmSessionForClearTypes(ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO));
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void addPlaybackPropertiesToIntent(
|
||||
MediaItem.PlaybackProperties playbackProperties, Intent intent, String extrasKeySuffix) {
|
||||
intent
|
||||
.putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType)
|
||||
.putExtra(
|
||||
AD_TAG_URI_EXTRA + extrasKeySuffix,
|
||||
playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null);
|
||||
if (playbackProperties.drmConfiguration != null) {
|
||||
addDrmConfigurationToIntent(playbackProperties.drmConfiguration, intent, extrasKeySuffix);
|
||||
}
|
||||
if (!playbackProperties.subtitles.isEmpty()) {
|
||||
checkState(playbackProperties.subtitles.size() == 1);
|
||||
MediaItem.Subtitle subtitle = playbackProperties.subtitles.get(0);
|
||||
intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, subtitle.uri.toString());
|
||||
intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitle.mimeType);
|
||||
intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, subtitle.language);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addDrmConfigurationToIntent(
|
||||
MediaItem.DrmConfiguration drmConfiguration, Intent intent, String extrasKeySuffix) {
|
||||
intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmConfiguration.uuid.toString());
|
||||
intent.putExtra(
|
||||
DRM_LICENSE_URI_EXTRA + extrasKeySuffix,
|
||||
drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : null);
|
||||
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmConfiguration.multiSession);
|
||||
intent.putExtra(
|
||||
DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix,
|
||||
drmConfiguration.forceDefaultLicenseUri);
|
||||
|
||||
String[] drmKeyRequestProperties = new String[drmConfiguration.requestHeaders.size() * 2];
|
||||
int index = 0;
|
||||
for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {
|
||||
drmKeyRequestProperties[index++] = entry.getKey();
|
||||
drmKeyRequestProperties[index++] = entry.getValue();
|
||||
}
|
||||
intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
|
||||
|
||||
List<Integer> drmSessionForClearTypes = drmConfiguration.sessionForClearTypes;
|
||||
if (!drmSessionForClearTypes.isEmpty()) {
|
||||
// Only video and audio together are supported.
|
||||
Assertions.checkState(
|
||||
drmSessionForClearTypes.size() == 2
|
||||
&& drmSessionForClearTypes.contains(C.TRACK_TYPE_VIDEO)
|
||||
&& drmSessionForClearTypes.contains(C.TRACK_TYPE_AUDIO));
|
||||
intent.putExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addClippingPropertiesToIntent(
|
||||
MediaItem.ClippingProperties clippingProperties, Intent intent, String extrasKeySuffix) {
|
||||
if (clippingProperties.startPositionMs != 0) {
|
||||
intent.putExtra(
|
||||
CLIP_START_POSITION_MS_EXTRA + extrasKeySuffix, clippingProperties.startPositionMs);
|
||||
}
|
||||
if (clippingProperties.endPositionMs != C.TIME_END_OF_SOURCE) {
|
||||
intent.putExtra(
|
||||
CLIP_END_POSITION_MS_EXTRA + extrasKeySuffix, clippingProperties.endPositionMs);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
@ -31,100 +33,43 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.ContentType;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.demo.Sample.UriSample;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.MediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||
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.hls.HlsMediaSource;
|
||||
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.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.RandomTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||
import com.google.android.exoplayer2.util.EventLogger;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||
public class PlayerActivity extends AppCompatActivity
|
||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
||||
|
||||
// Activity extras.
|
||||
|
||||
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
|
||||
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
|
||||
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
|
||||
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
|
||||
|
||||
// Actions.
|
||||
|
||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||
public static final String ACTION_VIEW_LIST =
|
||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||
|
||||
// Player configuration extras.
|
||||
|
||||
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
||||
public static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||
public static final String ABR_ALGORITHM_RANDOM = "random";
|
||||
|
||||
// Media item configuration extras.
|
||||
|
||||
public static final String URI_EXTRA = "uri";
|
||||
public static final String EXTENSION_EXTRA = "extension";
|
||||
public static final String IS_LIVE_EXTRA = "is_live";
|
||||
|
||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||
public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||
public static final String TUNNELING_EXTRA = "tunneling";
|
||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||
public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
|
||||
public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
|
||||
public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
|
||||
// For backwards compatibility only.
|
||||
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||
implements OnClickListener, PlaybackPreparer, StyledPlayerControlView.VisibilityListener {
|
||||
|
||||
// Saved instance state keys.
|
||||
|
||||
@ -134,25 +79,25 @@ public class PlayerActivity extends AppCompatActivity
|
||||
private static final String KEY_AUTO_PLAY = "auto_play";
|
||||
|
||||
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
||||
|
||||
static {
|
||||
DEFAULT_COOKIE_MANAGER = new CookieManager();
|
||||
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
}
|
||||
|
||||
private PlayerView playerView;
|
||||
private LinearLayout debugRootView;
|
||||
private Button selectTracksButton;
|
||||
private TextView debugTextView;
|
||||
private boolean isShowingTrackSelectionDialog;
|
||||
protected StyledPlayerView playerView;
|
||||
protected LinearLayout debugRootView;
|
||||
protected TextView debugTextView;
|
||||
protected SimpleExoPlayer player;
|
||||
|
||||
private boolean isShowingTrackSelectionDialog;
|
||||
private Button selectTracksButton;
|
||||
private DataSource.Factory dataSourceFactory;
|
||||
private SimpleExoPlayer player;
|
||||
private MediaSource mediaSource;
|
||||
private List<MediaItem> mediaItems;
|
||||
private DefaultTrackSelector trackSelector;
|
||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||
private DebugTextViewHelper debugViewHelper;
|
||||
private TrackGroupArray lastSeenTrackGroupArray;
|
||||
|
||||
private boolean startAutoPlay;
|
||||
private int startWindow;
|
||||
private long startPosition;
|
||||
@ -166,18 +111,13 @@ public class PlayerActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Intent intent = getIntent();
|
||||
String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
||||
if (sphericalStereoMode != null) {
|
||||
setTheme(R.style.PlayerTheme_Spherical);
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
dataSourceFactory = buildDataSourceFactory();
|
||||
dataSourceFactory = DemoUtil.getDataSourceFactory(/* context= */ this);
|
||||
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
||||
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
||||
}
|
||||
|
||||
setContentView(R.layout.player_activity);
|
||||
setContentView();
|
||||
debugRootView = findViewById(R.id.controls_root);
|
||||
debugTextView = findViewById(R.id.debug_text_view);
|
||||
selectTracksButton = findViewById(R.id.select_tracks_button);
|
||||
@ -187,21 +127,6 @@ public class PlayerActivity extends AppCompatActivity
|
||||
playerView.setControllerVisibilityListener(this);
|
||||
playerView.setErrorMessageProvider(new PlayerErrorMessageProvider());
|
||||
playerView.requestFocus();
|
||||
if (sphericalStereoMode != null) {
|
||||
int stereoMode;
|
||||
if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_MONO;
|
||||
} else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
|
||||
} else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
|
||||
} else {
|
||||
showToast(R.string.error_unrecognized_stereo_mode);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
((SphericalGLSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
trackSelectorParameters = savedInstanceState.getParcelable(KEY_TRACK_SELECTOR_PARAMETERS);
|
||||
@ -211,10 +136,6 @@ public class PlayerActivity extends AppCompatActivity
|
||||
} else {
|
||||
DefaultTrackSelector.ParametersBuilder builder =
|
||||
new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
|
||||
boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false);
|
||||
if (Util.SDK_INT >= 21 && tunneling) {
|
||||
builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this));
|
||||
}
|
||||
trackSelectorParameters = builder.build();
|
||||
clearStartPosition();
|
||||
}
|
||||
@ -280,8 +201,9 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (grantResults.length == 0) {
|
||||
// Empty results are triggered if a permission is requested while another request was already
|
||||
// pending and can be safely ignored in this case.
|
||||
@ -296,7 +218,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
updateTrackSelectorParameters();
|
||||
updateStartPosition();
|
||||
@ -330,14 +252,14 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
// PlaybackControlView.PlaybackPreparer implementation
|
||||
// PlaybackPreparer implementation
|
||||
|
||||
@Override
|
||||
public void preparePlayback() {
|
||||
player.retry();
|
||||
player.prepare();
|
||||
}
|
||||
|
||||
// PlaybackControlView.VisibilityListener implementation
|
||||
// PlayerControlView.VisibilityListener implementation
|
||||
|
||||
@Override
|
||||
public void onVisibilityChange(int visibility) {
|
||||
@ -346,214 +268,120 @@ public class PlayerActivity extends AppCompatActivity
|
||||
|
||||
// Internal methods
|
||||
|
||||
private void initializePlayer() {
|
||||
protected void setContentView() {
|
||||
setContentView(R.layout.player_activity);
|
||||
}
|
||||
|
||||
/** @return Whether initialization was successful. */
|
||||
protected boolean initializePlayer() {
|
||||
if (player == null) {
|
||||
Intent intent = getIntent();
|
||||
|
||||
mediaSource = createTopLevelMediaSource(intent);
|
||||
if (mediaSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrackSelection.Factory trackSelectionFactory;
|
||||
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
||||
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
||||
trackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
||||
} else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
|
||||
trackSelectionFactory = new RandomTrackSelection.Factory();
|
||||
} else {
|
||||
showToast(R.string.error_unrecognized_abr_algorithm);
|
||||
finish();
|
||||
return;
|
||||
mediaItems = createMediaItems(intent);
|
||||
if (mediaItems.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean preferExtensionDecoders =
|
||||
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||
intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||
RenderersFactory renderersFactory =
|
||||
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
||||
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
|
||||
MediaSourceFactory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(dataSourceFactory)
|
||||
.setAdsLoaderProvider(this::getAdsLoader)
|
||||
.setAdViewProvider(playerView);
|
||||
|
||||
trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
|
||||
trackSelector = new DefaultTrackSelector(/* context= */ this);
|
||||
trackSelector.setParameters(trackSelectorParameters);
|
||||
lastSeenTrackGroupArray = null;
|
||||
|
||||
player =
|
||||
new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
|
||||
.setMediaSourceFactory(mediaSourceFactory)
|
||||
.setTrackSelector(trackSelector)
|
||||
.build();
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
playerView.setPlayer(player);
|
||||
playerView.setPlaybackPreparer(this);
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
debugViewHelper.start();
|
||||
if (adsLoader != null) {
|
||||
adsLoader.setPlayer(player);
|
||||
}
|
||||
}
|
||||
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
||||
if (haveStartPosition) {
|
||||
player.seekTo(startWindow, startPosition);
|
||||
}
|
||||
player.prepare(mediaSource, !haveStartPosition, false);
|
||||
player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition);
|
||||
player.prepare();
|
||||
updateButtonVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MediaSource createTopLevelMediaSource(Intent intent) {
|
||||
private List<MediaItem> createMediaItems(Intent intent) {
|
||||
String action = intent.getAction();
|
||||
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
|
||||
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
|
||||
boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action);
|
||||
if (!actionIsListView && !IntentUtil.ACTION_VIEW.equals(action)) {
|
||||
showToast(getString(R.string.unexpected_intent_action, action));
|
||||
finish();
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Sample intentAsSample = Sample.createFromIntent(intent);
|
||||
UriSample[] samples =
|
||||
intentAsSample instanceof Sample.PlaylistSample
|
||||
? ((Sample.PlaylistSample) intentAsSample).children
|
||||
: new UriSample[] {(UriSample) intentAsSample};
|
||||
List<MediaItem> mediaItems =
|
||||
createMediaItems(intent, DemoUtil.getDownloadTracker(/* context= */ this));
|
||||
boolean hasAds = false;
|
||||
for (int i = 0; i < mediaItems.size(); i++) {
|
||||
MediaItem mediaItem = mediaItems.get(i);
|
||||
|
||||
boolean seenAdsTagUri = false;
|
||||
for (UriSample sample : samples) {
|
||||
seenAdsTagUri |= sample.adTagUri != null;
|
||||
if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
|
||||
if (!Util.checkCleartextTrafficPermitted(mediaItem)) {
|
||||
showToast(R.string.error_cleartext_not_permitted);
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
|
||||
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) {
|
||||
// The player will be reinitialized if the permission is granted.
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
MediaSource[] mediaSources = new MediaSource[samples.length];
|
||||
for (int i = 0; i < samples.length; i++) {
|
||||
mediaSources[i] = createLeafMediaSource(samples[i]);
|
||||
Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo;
|
||||
if (subtitleInfo != null) {
|
||||
if (Util.maybeRequestReadExternalStoragePermission(
|
||||
/* activity= */ this, subtitleInfo.uri)) {
|
||||
// The player will be reinitialized if the permission is granted.
|
||||
return null;
|
||||
MediaItem.DrmConfiguration drmConfiguration =
|
||||
checkNotNull(mediaItem.playbackProperties).drmConfiguration;
|
||||
if (drmConfiguration != null) {
|
||||
if (Util.SDK_INT < 18) {
|
||||
showToast(R.string.error_drm_unsupported_before_api_18);
|
||||
finish();
|
||||
return Collections.emptyList();
|
||||
} else if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmConfiguration.uuid)) {
|
||||
showToast(R.string.error_drm_unsupported_scheme);
|
||||
finish();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Format subtitleFormat =
|
||||
Format.createTextSampleFormat(
|
||||
/* id= */ null,
|
||||
subtitleInfo.mimeType,
|
||||
C.SELECTION_FLAG_DEFAULT,
|
||||
subtitleInfo.language);
|
||||
MediaSource subtitleMediaSource =
|
||||
new SingleSampleMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET);
|
||||
mediaSources[i] = new MergingMediaSource(mediaSources[i], subtitleMediaSource);
|
||||
}
|
||||
hasAds |= mediaItem.playbackProperties.adTagUri != null;
|
||||
}
|
||||
MediaSource mediaSource =
|
||||
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
||||
|
||||
if (seenAdsTagUri) {
|
||||
Uri adTagUri = samples[0].adTagUri;
|
||||
if (actionIsListView) {
|
||||
showToast(R.string.unsupported_ads_in_concatenation);
|
||||
} else {
|
||||
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||
releaseAdsLoader();
|
||||
loadedAdTagUri = adTagUri;
|
||||
}
|
||||
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri);
|
||||
if (adsMediaSource != null) {
|
||||
mediaSource = adsMediaSource;
|
||||
} else {
|
||||
showToast(R.string.ima_not_loaded);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!hasAds) {
|
||||
releaseAdsLoader();
|
||||
}
|
||||
|
||||
return mediaSource;
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
private MediaSource createLeafMediaSource(UriSample parameters) {
|
||||
Sample.DrmInfo drmInfo = parameters.drmInfo;
|
||||
int errorStringId = R.string.error_drm_unknown;
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager = null;
|
||||
if (drmInfo == null) {
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
} else if (Util.SDK_INT < 18) {
|
||||
errorStringId = R.string.error_drm_unsupported_before_api_18;
|
||||
} else if (!FrameworkMediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) {
|
||||
errorStringId = R.string.error_drm_unsupported_scheme;
|
||||
} else {
|
||||
MediaDrmCallback mediaDrmCallback =
|
||||
createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties);
|
||||
drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||
.setMultiSession(drmInfo.drmMultiSession)
|
||||
.build(mediaDrmCallback);
|
||||
}
|
||||
|
||||
if (drmSessionManager == null) {
|
||||
showToast(errorStringId);
|
||||
finish();
|
||||
private AdsLoader getAdsLoader(Uri adTagUri) {
|
||||
if (mediaItems.size() > 1) {
|
||||
showToast(R.string.unsupported_ads_in_playlist);
|
||||
releaseAdsLoader();
|
||||
return null;
|
||||
}
|
||||
|
||||
DownloadRequest downloadRequest =
|
||||
((DemoApplication) getApplication())
|
||||
.getDownloadTracker()
|
||||
.getDownloadRequest(parameters.uri);
|
||||
if (downloadRequest != null) {
|
||||
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
|
||||
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||
releaseAdsLoader();
|
||||
loadedAdTagUri = adTagUri;
|
||||
}
|
||||
return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager);
|
||||
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||
if (adsLoader == null) {
|
||||
adsLoader = new ImaAdsLoader(/* context= */ PlayerActivity.this, adTagUri);
|
||||
}
|
||||
adsLoader.setPlayer(player);
|
||||
return adsLoader;
|
||||
}
|
||||
|
||||
private MediaSource createLeafMediaSource(
|
||||
Uri uri, String extension, DrmSessionManager<?> drmSessionManager) {
|
||||
@ContentType int type = Util.inferContentType(uri, extension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpMediaDrmCallback createMediaDrmCallback(
|
||||
String licenseUrl, String[] keyRequestPropertiesArray) {
|
||||
HttpDataSource.Factory licenseDataSourceFactory =
|
||||
((DemoApplication) getApplication()).buildHttpDataSourceFactory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
|
||||
if (keyRequestPropertiesArray != null) {
|
||||
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
|
||||
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
|
||||
keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
return drmCallback;
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
protected void releasePlayer() {
|
||||
if (player != null) {
|
||||
updateTrackSelectorParameters();
|
||||
updateStartPosition();
|
||||
@ -561,7 +389,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
debugViewHelper = null;
|
||||
player.release();
|
||||
player = null;
|
||||
mediaSource = null;
|
||||
mediaItems = Collections.emptyList();
|
||||
trackSelector = null;
|
||||
}
|
||||
if (adsLoader != null) {
|
||||
@ -592,69 +420,12 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void clearStartPosition() {
|
||||
protected void clearStartPosition() {
|
||||
startAutoPlay = true;
|
||||
startWindow = C.INDEX_UNSET;
|
||||
startPosition = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/** Returns a new DataSource factory. */
|
||||
private DataSource.Factory buildDataSourceFactory() {
|
||||
return ((DemoApplication) getApplication()).buildDataSourceFactory();
|
||||
}
|
||||
|
||||
/** Returns an ads media source, reusing the ads loader if one exists. */
|
||||
@Nullable
|
||||
private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
|
||||
// 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.
|
||||
try {
|
||||
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
|
||||
if (adsLoader == null) {
|
||||
// Full class names used so the LINT.IfChange rule triggers should any of the classes move.
|
||||
// LINT.IfChange
|
||||
Constructor<? extends AdsLoader> loaderConstructor =
|
||||
loaderClass
|
||||
.asSubclass(AdsLoader.class)
|
||||
.getConstructor(android.content.Context.class, android.net.Uri.class);
|
||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
||||
}
|
||||
MediaSourceFactory adMediaSourceFactory =
|
||||
new MediaSourceFactory() {
|
||||
|
||||
private DrmSessionManager<?> drmSessionManager =
|
||||
DrmSessionManager.getDummyDrmSessionManager();
|
||||
|
||||
@Override
|
||||
public MediaSourceFactory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
|
||||
this.drmSessionManager =
|
||||
drmSessionManager != null
|
||||
? drmSessionManager
|
||||
: DrmSessionManager.getDummyDrmSessionManager();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaSource createMediaSource(Uri uri) {
|
||||
return PlayerActivity.this.createLeafMediaSource(
|
||||
uri, /* extension=*/ null, drmSessionManager);
|
||||
}
|
||||
|
||||
@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, playerView);
|
||||
} catch (ClassNotFoundException e) {
|
||||
// IMA extension not loaded.
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// User controls
|
||||
|
||||
private void updateButtonVisibility() {
|
||||
@ -691,7 +462,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
private class PlayerEventListener implements Player.EventListener {
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED) {
|
||||
showControls();
|
||||
}
|
||||
@ -699,7 +470,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException e) {
|
||||
public void onPlayerError(@NonNull ExoPlaybackException e) {
|
||||
if (isBehindLiveWindow(e)) {
|
||||
clearStartPosition();
|
||||
initializePlayer();
|
||||
@ -711,7 +482,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
public void onTracksChanged(
|
||||
@NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) {
|
||||
updateButtonVisibility();
|
||||
if (trackGroups != lastSeenTrackGroupArray) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
@ -733,7 +505,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||
private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {
|
||||
|
||||
@Override
|
||||
public Pair<Integer, String> getErrorMessage(ExoPlaybackException e) {
|
||||
@NonNull
|
||||
public Pair<Integer, String> getErrorMessage(@NonNull ExoPlaybackException e) {
|
||||
String errorString = getString(R.string.error_generic);
|
||||
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
||||
Exception cause = e.getRendererException();
|
||||
@ -763,4 +536,15 @@ public class PlayerActivity extends AppCompatActivity
|
||||
return Pair.create(0, errorString);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<MediaItem> createMediaItems(Intent intent, DownloadTracker downloadTracker) {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) {
|
||||
@Nullable
|
||||
DownloadRequest downloadRequest =
|
||||
downloadTracker.getDownloadRequest(checkNotNull(item.playbackProperties).uri);
|
||||
mediaItems.add(downloadRequest != null ? downloadRequest.toMediaItem() : item);
|
||||
}
|
||||
return mediaItems;
|
||||
}
|
||||
}
|
||||
|
@ -1,236 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA;
|
||||
import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
/* package */ abstract class Sample {
|
||||
|
||||
public static final class UriSample extends Sample {
|
||||
|
||||
public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) {
|
||||
String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
|
||||
String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
|
||||
boolean isLive =
|
||||
intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false);
|
||||
Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
|
||||
return new UriSample(
|
||||
/* name= */ null,
|
||||
uri,
|
||||
extension,
|
||||
isLive,
|
||||
DrmInfo.createFromIntent(intent, extrasKeySuffix),
|
||||
adTagUri,
|
||||
/* sphericalStereoMode= */ null,
|
||||
SubtitleInfo.createFromIntent(intent, extrasKeySuffix));
|
||||
}
|
||||
|
||||
public final Uri uri;
|
||||
public final String extension;
|
||||
public final boolean isLive;
|
||||
public final DrmInfo drmInfo;
|
||||
public final Uri adTagUri;
|
||||
@Nullable public final String sphericalStereoMode;
|
||||
@Nullable SubtitleInfo subtitleInfo;
|
||||
|
||||
public UriSample(
|
||||
String name,
|
||||
Uri uri,
|
||||
String extension,
|
||||
boolean isLive,
|
||||
DrmInfo drmInfo,
|
||||
Uri adTagUri,
|
||||
@Nullable String sphericalStereoMode,
|
||||
@Nullable SubtitleInfo subtitleInfo) {
|
||||
super(name);
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.isLive = isLive;
|
||||
this.drmInfo = drmInfo;
|
||||
this.adTagUri = adTagUri;
|
||||
this.sphericalStereoMode = sphericalStereoMode;
|
||||
this.subtitleInfo = subtitleInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToIntent(Intent intent) {
|
||||
intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
|
||||
intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive);
|
||||
intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
|
||||
addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ "");
|
||||
}
|
||||
|
||||
public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) {
|
||||
intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString());
|
||||
intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive);
|
||||
addPlayerConfigToIntent(intent, extrasKeySuffix);
|
||||
}
|
||||
|
||||
private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) {
|
||||
intent
|
||||
.putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension)
|
||||
.putExtra(
|
||||
AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null);
|
||||
if (drmInfo != null) {
|
||||
drmInfo.addToIntent(intent, extrasKeySuffix);
|
||||
}
|
||||
if (subtitleInfo != null) {
|
||||
subtitleInfo.addToIntent(intent, extrasKeySuffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PlaylistSample extends Sample {
|
||||
|
||||
public final UriSample[] children;
|
||||
|
||||
public PlaylistSample(String name, UriSample... children) {
|
||||
super(name);
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToIntent(Intent intent) {
|
||||
intent.setAction(PlayerActivity.ACTION_VIEW_LIST);
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DrmInfo {
|
||||
|
||||
public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) {
|
||||
String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
|
||||
String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
|
||||
if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
|
||||
return null;
|
||||
}
|
||||
String drmSchemeExtra =
|
||||
intent.hasExtra(schemeKey)
|
||||
? intent.getStringExtra(schemeKey)
|
||||
: intent.getStringExtra(schemeUuidKey);
|
||||
UUID drmScheme = Util.getDrmUuid(drmSchemeExtra);
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix);
|
||||
String[] keyRequestPropertiesArray =
|
||||
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
|
||||
boolean drmMultiSession =
|
||||
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
|
||||
return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession);
|
||||
}
|
||||
|
||||
public final UUID drmScheme;
|
||||
public final String drmLicenseUrl;
|
||||
public final String[] drmKeyRequestProperties;
|
||||
public final boolean drmMultiSession;
|
||||
|
||||
public DrmInfo(
|
||||
UUID drmScheme,
|
||||
String drmLicenseUrl,
|
||||
String[] drmKeyRequestProperties,
|
||||
boolean drmMultiSession) {
|
||||
this.drmScheme = drmScheme;
|
||||
this.drmLicenseUrl = drmLicenseUrl;
|
||||
this.drmKeyRequestProperties = drmKeyRequestProperties;
|
||||
this.drmMultiSession = drmMultiSession;
|
||||
}
|
||||
|
||||
public void addToIntent(Intent intent, String extrasKeySuffix) {
|
||||
Assertions.checkNotNull(intent);
|
||||
intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString());
|
||||
intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl);
|
||||
intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
|
||||
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class SubtitleInfo {
|
||||
|
||||
@Nullable
|
||||
public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) {
|
||||
if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) {
|
||||
return null;
|
||||
}
|
||||
return new SubtitleInfo(
|
||||
Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)),
|
||||
intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix),
|
||||
intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix));
|
||||
}
|
||||
|
||||
public final Uri uri;
|
||||
public final String mimeType;
|
||||
@Nullable public final String language;
|
||||
|
||||
public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) {
|
||||
this.uri = Assertions.checkNotNull(uri);
|
||||
this.mimeType = Assertions.checkNotNull(mimeType);
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
public void addToIntent(Intent intent, String extrasKeySuffix) {
|
||||
intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString());
|
||||
intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType);
|
||||
intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language);
|
||||
}
|
||||
}
|
||||
|
||||
public static Sample createFromIntent(Intent intent) {
|
||||
if (ACTION_VIEW_LIST.equals(intent.getAction())) {
|
||||
ArrayList<String> intentUris = new ArrayList<>();
|
||||
int index = 0;
|
||||
while (intent.hasExtra(URI_EXTRA + "_" + index)) {
|
||||
intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index));
|
||||
index++;
|
||||
}
|
||||
UriSample[] children = new UriSample[intentUris.size()];
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
Uri uri = Uri.parse(intentUris.get(i));
|
||||
children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
}
|
||||
return new PlaylistSample(/* name= */ null, children);
|
||||
} else {
|
||||
return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ "");
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable public final String name;
|
||||
|
||||
public Sample(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public abstract void addToIntent(Intent intent);
|
||||
}
|
@ -15,9 +15,15 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
@ -38,47 +44,50 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.RenderersFactory;
|
||||
import com.google.android.exoplayer2.demo.Sample.DrmInfo;
|
||||
import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
|
||||
import com.google.android.exoplayer2.demo.Sample.UriSample;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
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.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** An activity for selecting from a list of media samples. */
|
||||
public class SampleChooserActivity extends AppCompatActivity
|
||||
implements DownloadTracker.Listener, OnChildClickListener {
|
||||
|
||||
private static final String TAG = "SampleChooserActivity";
|
||||
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position";
|
||||
private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position";
|
||||
|
||||
private String[] uris;
|
||||
private boolean useExtensionRenderers;
|
||||
private DownloadTracker downloadTracker;
|
||||
private SampleAdapter sampleAdapter;
|
||||
private MenuItem preferExtensionDecodersMenuItem;
|
||||
private MenuItem randomAbrMenuItem;
|
||||
private MenuItem tunnelingMenuItem;
|
||||
private ExpandableListView sampleListView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.sample_chooser_activity);
|
||||
sampleAdapter = new SampleAdapter();
|
||||
ExpandableListView sampleListView = findViewById(R.id.sample_list);
|
||||
sampleListView = findViewById(R.id.sample_list);
|
||||
|
||||
sampleListView.setAdapter(sampleAdapter);
|
||||
sampleListView.setOnChildClickListener(this);
|
||||
|
||||
@ -104,9 +113,8 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
Arrays.sort(uris);
|
||||
}
|
||||
|
||||
DemoApplication application = (DemoApplication) getApplication();
|
||||
useExtensionRenderers = application.useExtensionRenderers();
|
||||
downloadTracker = application.getDownloadTracker();
|
||||
useExtensionRenderers = DemoUtil.useExtensionRenderers();
|
||||
downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this);
|
||||
loadSample();
|
||||
|
||||
// Start the download service if it should be running but it's not currently.
|
||||
@ -126,11 +134,6 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
inflater.inflate(R.menu.sample_chooser_menu, menu);
|
||||
preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders);
|
||||
preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers);
|
||||
randomAbrMenuItem = menu.findItem(R.id.random_abr);
|
||||
tunnelingMenuItem = menu.findItem(R.id.tunneling);
|
||||
if (Util.SDK_INT < 21) {
|
||||
tunnelingMenuItem.setEnabled(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -161,6 +164,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
@Override
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (grantResults.length == 0) {
|
||||
// Empty results are triggered if a permission is requested while another request was already
|
||||
// pending and can be safely ignored in this case.
|
||||
@ -176,7 +180,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
private void loadSample() {
|
||||
Assertions.checkNotNull(uris);
|
||||
checkNotNull(uris);
|
||||
|
||||
for (int i = 0; i < uris.length; i++) {
|
||||
Uri uri = Uri.parse(uris[i]);
|
||||
@ -189,67 +193,69 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
loaderTask.execute(uris);
|
||||
}
|
||||
|
||||
private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
|
||||
private void onPlaylistGroups(final List<PlaylistGroup> groups, boolean sawError) {
|
||||
if (sawError) {
|
||||
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
sampleAdapter.setSampleGroups(groups);
|
||||
sampleAdapter.setPlaylistGroups(groups);
|
||||
|
||||
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
|
||||
int groupPosition = preferences.getInt(GROUP_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||
int childPosition = preferences.getInt(CHILD_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||
// Clear the group and child position if either are unset or if either are out of bounds.
|
||||
if (groupPosition != -1
|
||||
&& childPosition != -1
|
||||
&& groupPosition < groups.size()
|
||||
&& childPosition < groups.get(groupPosition).playlists.size()) {
|
||||
sampleListView.expandGroup(groupPosition); // shouldExpandGroup does not work without this.
|
||||
sampleListView.setSelectedChild(groupPosition, childPosition, /* shouldExpandGroup= */ true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onChildClick(
|
||||
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
|
||||
Sample sample = (Sample) view.getTag();
|
||||
// Save the selected item first to be able to restore it if the tested code crashes.
|
||||
SharedPreferences.Editor prefEditor = getPreferences(MODE_PRIVATE).edit();
|
||||
prefEditor.putInt(GROUP_POSITION_PREFERENCE_KEY, groupPosition);
|
||||
prefEditor.putInt(CHILD_POSITION_PREFERENCE_KEY, childPosition);
|
||||
prefEditor.apply();
|
||||
|
||||
PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag();
|
||||
Intent intent = new Intent(this, PlayerActivity.class);
|
||||
intent.putExtra(
|
||||
PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||
IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
String abrAlgorithm =
|
||||
isNonNullAndChecked(randomAbrMenuItem)
|
||||
? PlayerActivity.ABR_ALGORITHM_RANDOM
|
||||
: PlayerActivity.ABR_ALGORITHM_DEFAULT;
|
||||
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||
intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
|
||||
sample.addToIntent(intent);
|
||||
IntentUtil.addToIntent(playlistHolder.mediaItems, intent);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onSampleDownloadButtonClicked(Sample sample) {
|
||||
int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);
|
||||
private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) {
|
||||
int downloadUnsupportedStringId = getDownloadUnsupportedStringId(playlistHolder);
|
||||
if (downloadUnsupportedStringId != 0) {
|
||||
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} else {
|
||||
UriSample uriSample = (UriSample) sample;
|
||||
RenderersFactory renderersFactory =
|
||||
((DemoApplication) getApplication())
|
||||
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
DemoUtil.buildRenderersFactory(
|
||||
/* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||
downloadTracker.toggleDownload(
|
||||
getSupportFragmentManager(),
|
||||
sample.name,
|
||||
uriSample.uri,
|
||||
uriSample.extension,
|
||||
renderersFactory);
|
||||
getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory);
|
||||
}
|
||||
}
|
||||
|
||||
private int getDownloadUnsupportedStringId(Sample sample) {
|
||||
if (sample instanceof PlaylistSample) {
|
||||
private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) {
|
||||
if (playlistHolder.mediaItems.size() > 1) {
|
||||
return R.string.download_playlist_unsupported;
|
||||
}
|
||||
UriSample uriSample = (UriSample) sample;
|
||||
if (uriSample.drmInfo != null) {
|
||||
return R.string.download_drm_unsupported;
|
||||
}
|
||||
if (uriSample.isLive) {
|
||||
return R.string.download_live_unsupported;
|
||||
}
|
||||
if (uriSample.adTagUri != null) {
|
||||
MediaItem.PlaybackProperties playbackProperties =
|
||||
checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties);
|
||||
if (playbackProperties.adTagUri != null) {
|
||||
return R.string.download_ads_unsupported;
|
||||
}
|
||||
String scheme = uriSample.uri.getScheme();
|
||||
String scheme = playbackProperties.uri.getScheme();
|
||||
if (!("http".equals(scheme) || "https".equals(scheme))) {
|
||||
return R.string.download_scheme_unsupported;
|
||||
}
|
||||
@ -261,22 +267,20 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
return menuItem != null && menuItem.isChecked();
|
||||
}
|
||||
|
||||
private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> {
|
||||
private final class SampleListLoader extends AsyncTask<String, Void, List<PlaylistGroup>> {
|
||||
|
||||
private boolean sawError;
|
||||
|
||||
@Override
|
||||
protected List<SampleGroup> doInBackground(String... uris) {
|
||||
List<SampleGroup> result = new ArrayList<>();
|
||||
protected List<PlaylistGroup> doInBackground(String... uris) {
|
||||
List<PlaylistGroup> result = new ArrayList<>();
|
||||
Context context = getApplicationContext();
|
||||
String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
|
||||
DataSource dataSource =
|
||||
new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false);
|
||||
DataSource dataSource = DemoUtil.getDataSourceFactory(context).createDataSource();
|
||||
for (String uri : uris) {
|
||||
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
|
||||
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
|
||||
try {
|
||||
readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
|
||||
readPlaylistGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error loading sample list: " + uri, e);
|
||||
sawError = true;
|
||||
@ -288,21 +292,23 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<SampleGroup> result) {
|
||||
onSampleGroups(result, sawError);
|
||||
protected void onPostExecute(List<PlaylistGroup> result) {
|
||||
onPlaylistGroups(result, sawError);
|
||||
}
|
||||
|
||||
private void readSampleGroups(JsonReader reader, List<SampleGroup> groups) throws IOException {
|
||||
private void readPlaylistGroups(JsonReader reader, List<PlaylistGroup> groups)
|
||||
throws IOException {
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
readSampleGroup(reader, groups);
|
||||
readPlaylistGroup(reader, groups);
|
||||
}
|
||||
reader.endArray();
|
||||
}
|
||||
|
||||
private void readSampleGroup(JsonReader reader, List<SampleGroup> groups) throws IOException {
|
||||
private void readPlaylistGroup(JsonReader reader, List<PlaylistGroup> groups)
|
||||
throws IOException {
|
||||
String groupName = "";
|
||||
ArrayList<Sample> samples = new ArrayList<>();
|
||||
ArrayList<PlaylistHolder> playlistHolders = new ArrayList<>();
|
||||
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
@ -314,7 +320,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
case "samples":
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
samples.add(readEntry(reader, false));
|
||||
playlistHolders.add(readEntry(reader, false));
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
@ -327,33 +333,26 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
}
|
||||
reader.endObject();
|
||||
|
||||
SampleGroup group = getGroup(groupName, groups);
|
||||
group.samples.addAll(samples);
|
||||
PlaylistGroup group = getGroup(groupName, groups);
|
||||
group.playlists.addAll(playlistHolders);
|
||||
}
|
||||
|
||||
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
||||
String sampleName = null;
|
||||
private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
||||
Uri uri = null;
|
||||
String extension = null;
|
||||
boolean isLive = false;
|
||||
String drmScheme = null;
|
||||
String drmLicenseUrl = null;
|
||||
String[] drmKeyRequestProperties = null;
|
||||
boolean drmMultiSession = false;
|
||||
ArrayList<UriSample> playlistSamples = null;
|
||||
String adTagUri = null;
|
||||
String sphericalStereoMode = null;
|
||||
List<Sample.SubtitleInfo> subtitleInfos = new ArrayList<>();
|
||||
String title = null;
|
||||
ArrayList<PlaylistHolder> children = null;
|
||||
Uri subtitleUri = null;
|
||||
String subtitleMimeType = null;
|
||||
String subtitleLanguage = null;
|
||||
|
||||
MediaItem.Builder mediaItem = new MediaItem.Builder();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
String name = reader.nextName();
|
||||
switch (name) {
|
||||
case "name":
|
||||
sampleName = reader.nextString();
|
||||
title = reader.nextString();
|
||||
break;
|
||||
case "uri":
|
||||
uri = Uri.parse(reader.nextString());
|
||||
@ -361,44 +360,42 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
case "extension":
|
||||
extension = reader.nextString();
|
||||
break;
|
||||
case "drm_scheme":
|
||||
drmScheme = reader.nextString();
|
||||
case "clip_start_position_ms":
|
||||
mediaItem.setClipStartPositionMs(reader.nextLong());
|
||||
break;
|
||||
case "is_live":
|
||||
isLive = reader.nextBoolean();
|
||||
break;
|
||||
case "drm_license_url":
|
||||
drmLicenseUrl = reader.nextString();
|
||||
break;
|
||||
case "drm_key_request_properties":
|
||||
ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
drmKeyRequestPropertiesList.add(reader.nextName());
|
||||
drmKeyRequestPropertiesList.add(reader.nextString());
|
||||
}
|
||||
reader.endObject();
|
||||
drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
|
||||
break;
|
||||
case "drm_multi_session":
|
||||
drmMultiSession = reader.nextBoolean();
|
||||
break;
|
||||
case "playlist":
|
||||
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
|
||||
playlistSamples = new ArrayList<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true));
|
||||
}
|
||||
reader.endArray();
|
||||
case "clip_end_position_ms":
|
||||
mediaItem.setClipEndPositionMs(reader.nextLong());
|
||||
break;
|
||||
case "ad_tag_uri":
|
||||
adTagUri = reader.nextString();
|
||||
mediaItem.setAdTagUri(reader.nextString());
|
||||
break;
|
||||
case "spherical_stereo_mode":
|
||||
Assertions.checkState(
|
||||
!insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode");
|
||||
sphericalStereoMode = reader.nextString();
|
||||
case "drm_scheme":
|
||||
mediaItem.setDrmUuid(Util.getDrmUuid(reader.nextString()));
|
||||
break;
|
||||
case "drm_license_uri":
|
||||
case "drm_license_url": // For backward compatibility only.
|
||||
mediaItem.setDrmLicenseUri(reader.nextString());
|
||||
break;
|
||||
case "drm_key_request_properties":
|
||||
Map<String, String> requestHeaders = new HashMap<>();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
requestHeaders.put(reader.nextName(), reader.nextString());
|
||||
}
|
||||
reader.endObject();
|
||||
mediaItem.setDrmLicenseRequestHeaders(requestHeaders);
|
||||
break;
|
||||
case "drm_session_for_clear_content":
|
||||
if (reader.nextBoolean()) {
|
||||
mediaItem.setDrmSessionForClearTypes(
|
||||
ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO));
|
||||
}
|
||||
break;
|
||||
case "drm_multi_session":
|
||||
mediaItem.setDrmMultiSession(reader.nextBoolean());
|
||||
break;
|
||||
case "drm_force_default_license_uri":
|
||||
mediaItem.setDrmForceDefaultLicenseUri(reader.nextBoolean());
|
||||
break;
|
||||
case "subtitle_uri":
|
||||
subtitleUri = Uri.parse(reader.nextString());
|
||||
@ -409,72 +406,76 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
case "subtitle_language":
|
||||
subtitleLanguage = reader.nextString();
|
||||
break;
|
||||
case "playlist":
|
||||
checkState(!insidePlaylist, "Invalid nesting of playlists");
|
||||
children = new ArrayList<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
children.add(readEntry(reader, /* insidePlaylist= */ true));
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
default:
|
||||
throw new ParserException("Unsupported attribute name: " + name);
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
DrmInfo drmInfo =
|
||||
drmScheme == null
|
||||
? null
|
||||
: new DrmInfo(
|
||||
Util.getDrmUuid(drmScheme),
|
||||
drmLicenseUrl,
|
||||
drmKeyRequestProperties,
|
||||
drmMultiSession);
|
||||
Sample.SubtitleInfo subtitleInfo =
|
||||
subtitleUri == null
|
||||
? null
|
||||
: new Sample.SubtitleInfo(
|
||||
|
||||
if (children != null) {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
for (int i = 0; i < children.size(); i++) {
|
||||
mediaItems.addAll(children.get(i).mediaItems);
|
||||
}
|
||||
return new PlaylistHolder(title, mediaItems);
|
||||
} else {
|
||||
@Nullable
|
||||
String adaptiveMimeType =
|
||||
Util.getAdaptiveMimeTypeForContentType(Util.inferContentType(uri, extension));
|
||||
mediaItem
|
||||
.setUri(uri)
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
|
||||
.setMimeType(adaptiveMimeType);
|
||||
if (subtitleUri != null) {
|
||||
MediaItem.Subtitle subtitle =
|
||||
new MediaItem.Subtitle(
|
||||
subtitleUri,
|
||||
Assertions.checkNotNull(
|
||||
checkNotNull(
|
||||
subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."),
|
||||
subtitleLanguage);
|
||||
if (playlistSamples != null) {
|
||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
|
||||
return new PlaylistSample(sampleName, playlistSamplesArray);
|
||||
} else {
|
||||
return new UriSample(
|
||||
sampleName,
|
||||
uri,
|
||||
extension,
|
||||
isLive,
|
||||
drmInfo,
|
||||
adTagUri != null ? Uri.parse(adTagUri) : null,
|
||||
sphericalStereoMode,
|
||||
subtitleInfo);
|
||||
mediaItem.setSubtitles(Collections.singletonList(subtitle));
|
||||
}
|
||||
return new PlaylistHolder(title, Collections.singletonList(mediaItem.build()));
|
||||
}
|
||||
}
|
||||
|
||||
private SampleGroup getGroup(String groupName, List<SampleGroup> groups) {
|
||||
private PlaylistGroup getGroup(String groupName, List<PlaylistGroup> groups) {
|
||||
for (int i = 0; i < groups.size(); i++) {
|
||||
if (Util.areEqual(groupName, groups.get(i).title)) {
|
||||
return groups.get(i);
|
||||
}
|
||||
}
|
||||
SampleGroup group = new SampleGroup(groupName);
|
||||
PlaylistGroup group = new PlaylistGroup(groupName);
|
||||
groups.add(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
|
||||
|
||||
private List<SampleGroup> sampleGroups;
|
||||
private List<PlaylistGroup> playlistGroups;
|
||||
|
||||
public SampleAdapter() {
|
||||
sampleGroups = Collections.emptyList();
|
||||
playlistGroups = Collections.emptyList();
|
||||
}
|
||||
|
||||
public void setSampleGroups(List<SampleGroup> sampleGroups) {
|
||||
this.sampleGroups = sampleGroups;
|
||||
public void setPlaylistGroups(List<PlaylistGroup> playlistGroups) {
|
||||
this.playlistGroups = playlistGroups;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sample getChild(int groupPosition, int childPosition) {
|
||||
return getGroup(groupPosition).samples.get(childPosition);
|
||||
public PlaylistHolder getChild(int groupPosition, int childPosition) {
|
||||
return getGroup(groupPosition).playlists.get(childPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -483,8 +484,12 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
|
||||
View convertView, ViewGroup parent) {
|
||||
public View getChildView(
|
||||
int groupPosition,
|
||||
int childPosition,
|
||||
boolean isLastChild,
|
||||
View convertView,
|
||||
ViewGroup parent) {
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
|
||||
@ -498,12 +503,12 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public int getChildrenCount(int groupPosition) {
|
||||
return getGroup(groupPosition).samples.size();
|
||||
return getGroup(groupPosition).playlists.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleGroup getGroup(int groupPosition) {
|
||||
return sampleGroups.get(groupPosition);
|
||||
public PlaylistGroup getGroup(int groupPosition) {
|
||||
return playlistGroups.get(groupPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -512,8 +517,8 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
|
||||
ViewGroup parent) {
|
||||
public View getGroupView(
|
||||
int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
|
||||
View view = convertView;
|
||||
if (view == null) {
|
||||
view =
|
||||
@ -526,7 +531,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public int getGroupCount() {
|
||||
return sampleGroups.size();
|
||||
return playlistGroups.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -541,18 +546,19 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onSampleDownloadButtonClicked((Sample) view.getTag());
|
||||
onSampleDownloadButtonClicked((PlaylistHolder) view.getTag());
|
||||
}
|
||||
|
||||
private void initializeChildView(View view, Sample sample) {
|
||||
view.setTag(sample);
|
||||
private void initializeChildView(View view, PlaylistHolder playlistHolder) {
|
||||
view.setTag(playlistHolder);
|
||||
TextView sampleTitle = view.findViewById(R.id.sample_title);
|
||||
sampleTitle.setText(sample.name);
|
||||
sampleTitle.setText(playlistHolder.title);
|
||||
|
||||
boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;
|
||||
boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);
|
||||
boolean canDownload = getDownloadUnsupportedStringId(playlistHolder) == 0;
|
||||
boolean isDownloaded =
|
||||
canDownload && downloadTracker.isDownloaded(playlistHolder.mediaItems.get(0));
|
||||
ImageButton downloadButton = view.findViewById(R.id.download_button);
|
||||
downloadButton.setTag(sample);
|
||||
downloadButton.setTag(playlistHolder);
|
||||
downloadButton.setColorFilter(
|
||||
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666);
|
||||
downloadButton.setImageResource(
|
||||
@ -560,15 +566,26 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SampleGroup {
|
||||
private static final class PlaylistHolder {
|
||||
|
||||
public final String title;
|
||||
public final List<Sample> samples;
|
||||
public final List<MediaItem> mediaItems;
|
||||
|
||||
public SampleGroup(String title) {
|
||||
private PlaylistHolder(String title, List<MediaItem> mediaItems) {
|
||||
checkArgument(!mediaItems.isEmpty());
|
||||
this.title = title;
|
||||
this.samples = new ArrayList<>();
|
||||
this.mediaItems = Collections.unmodifiableList(new ArrayList<>(mediaItems));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PlaylistGroup {
|
||||
|
||||
public final String title;
|
||||
public final List<PlaylistHolder> playlists;
|
||||
|
||||
public PlaylistGroup(String title) {
|
||||
this.title = title;
|
||||
this.playlists = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
@ -212,6 +213,7 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// We need to own the view to let tab layout work correctly on all API levels. We can't use
|
||||
// AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using
|
||||
@ -223,16 +225,14 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
public void onDismiss(@NonNull DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
onDismissListener.onDismiss(dialog);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
||||
View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false);
|
||||
TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout);
|
||||
ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager);
|
||||
@ -286,10 +286,11 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||
private final class FragmentAdapter extends FragmentPagerAdapter {
|
||||
|
||||
public FragmentAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Fragment getItem(int position) {
|
||||
return tabFragments.valueAt(position);
|
||||
}
|
||||
@ -299,7 +300,6 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||
return tabFragments.size();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return getTrackTypeString(getResources(), tabTrackTypes.get(position));
|
||||
@ -341,7 +341,6 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||
this.allowMultipleOverrides = allowMultipleOverrides;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater,
|
||||
@ -360,7 +359,8 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackSelectionChanged(boolean isDisabled, List<SelectionOverride> overrides) {
|
||||
public void onTrackSelectionChanged(
|
||||
boolean isDisabled, @NonNull List<SelectionOverride> overrides) {
|
||||
this.isDisabled = isDisabled;
|
||||
this.overrides = overrides;
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.demo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -15,14 +15,17 @@
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view"
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView android:id="@+id/player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
app:show_shuffle_button="true"
|
||||
app:show_subtitle_button="true"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
@ -19,12 +19,4 @@
|
||||
android:title="@string/prefer_extension_decoders"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
<item android:id="@+id/random_abr"
|
||||
android:title="@string/random_abr"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
<item android:id="@+id/tunneling"
|
||||
android:title="@string/tunneling"
|
||||
android:checkable="true"
|
||||
app:showAsAction="never"/>
|
||||
</menu>
|
||||
|
@ -25,16 +25,10 @@
|
||||
|
||||
<string name="error_generic">Playback failed</string>
|
||||
|
||||
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
|
||||
|
||||
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
|
||||
|
||||
<string name="error_drm_unsupported_before_api_18">Protected content not supported on API levels below 18</string>
|
||||
<string name="error_drm_unsupported_before_api_18">DRM content not supported on API levels below 18</string>
|
||||
|
||||
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||
|
||||
<string name="error_drm_unknown">An unknown DRM error occurred</string>
|
||||
|
||||
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
|
||||
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||
@ -51,15 +45,13 @@
|
||||
|
||||
<string name="sample_list_load_error">One or more sample lists failed to load</string>
|
||||
|
||||
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
||||
|
||||
<string name="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
|
||||
<string name="unsupported_ads_in_playlist">Playing without ads, as ads are not supported in playlists</string>
|
||||
|
||||
<string name="download_start_error">Failed to start download</string>
|
||||
|
||||
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
|
||||
<string name="download_start_error_offline_license">Failed to obtain offline license</string>
|
||||
|
||||
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string>
|
||||
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
|
||||
|
||||
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
|
||||
|
||||
@ -69,8 +61,4 @@
|
||||
|
||||
<string name="prefer_extension_decoders">Prefer extension decoders</string>
|
||||
|
||||
<string name="random_abr">Enable random ABR</string>
|
||||
|
||||
<string name="tunneling">Request multimedia tunneling</string>
|
||||
|
||||
</resources>
|
||||
|
@ -23,8 +23,4 @@
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="PlayerTheme.Spherical">
|
||||
<item name="surface_type">spherical_gl_surface_view</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
@ -28,11 +28,11 @@ import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
@ -184,13 +184,12 @@ public final class MainActivity extends Activity {
|
||||
ACTION_VIEW.equals(action)
|
||||
? Assertions.checkNotNull(intent.getData())
|
||||
: Uri.parse(DEFAULT_MEDIA_URI);
|
||||
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||
DrmSessionManager drmSessionManager;
|
||||
if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
|
||||
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
|
||||
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
|
||||
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
|
||||
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
|
||||
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory();
|
||||
HttpMediaDrmCallback drmCallback =
|
||||
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||
drmSessionManager =
|
||||
@ -201,27 +200,26 @@ public final class MainActivity extends Activity {
|
||||
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||
}
|
||||
|
||||
DataSource.Factory dataSourceFactory =
|
||||
new DefaultDataSourceFactory(
|
||||
this, Util.getUserAgent(this, getString(R.string.application_name)));
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
|
||||
MediaSource mediaSource;
|
||||
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
|
||||
if (type == C.TYPE_DASH) {
|
||||
mediaSource =
|
||||
new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else if (type == C.TYPE_OTHER) {
|
||||
mediaSource =
|
||||
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.setDrmSessionManager(drmSessionManager)
|
||||
.createMediaSource(uri);
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
|
||||
player.prepare(mediaSource);
|
||||
player.setPlayWhenReady(true);
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
player.play();
|
||||
player.setRepeatMode(Player.REPEAT_MODE_ALL);
|
||||
|
||||
surfaceControl =
|
||||
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.surfacedemo;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -39,7 +39,7 @@ git clone https://github.com/google/cpu_features
|
||||
|
||||
```
|
||||
cd "${AV1_EXT_PATH}/jni" && \
|
||||
git clone https://chromium.googlesource.com/codecs/libgav1 libgav1
|
||||
git clone https://chromium.googlesource.com/codecs/libgav1
|
||||
```
|
||||
|
||||
* Fetch Abseil:
|
||||
@ -109,19 +109,22 @@ To try out playback using the extension in the [demo application][], see
|
||||
There are two possibilities for rendering the output `Libgav1VideoRenderer`
|
||||
gets from the libgav1 decoder:
|
||||
|
||||
* GL rendering using GL shader for color space conversion
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, enable this option by
|
||||
setting `surface_type` of `PlayerView` to be
|
||||
`video_decoder_gl_surface_view`.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message
|
||||
of type `C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER` with an instance of
|
||||
`VideoDecoderOutputBufferRenderer` as its object.
|
||||
* GL rendering using GL shader for color space conversion
|
||||
|
||||
* Native rendering using `ANativeWindow`
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, this option is enabled
|
||||
by default.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of
|
||||
type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object.
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, enable this option
|
||||
by setting `surface_type` of `PlayerView` to be
|
||||
`video_decoder_gl_surface_view`.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a
|
||||
message of type `Renderer.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER`
|
||||
with an instance of `VideoDecoderOutputBufferRenderer` as its object.
|
||||
|
||||
* Native rendering using `ANativeWindow`
|
||||
|
||||
* If you are using `SimpleExoPlayer` with `PlayerView`, this option is
|
||||
enabled by default.
|
||||
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a
|
||||
message of type `Renderer.MSG_SET_SURFACE` with an instance of
|
||||
`SurfaceView` as its object.
|
||||
|
||||
Note: Although the default option uses `ANativeWindow`, based on our testing the
|
||||
GL rendering mode has better performance, so should be preferred
|
||||
|
@ -11,22 +11,10 @@
|
||||
// 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'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
// Debug CMake build type causes video frames to drop,
|
||||
@ -36,35 +24,28 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This option resolves the problem of finding libgav1JNI.so
|
||||
// on multiple paths. The first one found is picked.
|
||||
packagingOptions {
|
||||
pickFirst 'lib/arm64-v8a/libgav1JNI.so'
|
||||
pickFirst 'lib/armeabi-v7a/libgav1JNI.so'
|
||||
pickFirst 'lib/x86/libgav1JNI.so'
|
||||
pickFirst 'lib/x86_64/libgav1JNI.so'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
// As native JNI library build is invoked from gradle, this is
|
||||
// not needed. However, it exposes the built library and keeps
|
||||
// consistency with the other extensions.
|
||||
jniLibs.srcDir 'src/main/libs'
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the native build only if libgav1 is present, to avoid gradle sync
|
||||
// failures if libgav1 hasn't been checked out according to the README and CMake
|
||||
// isn't installed.
|
||||
// Configure the native build only if libgav1 is present to avoid gradle sync
|
||||
// failures if libgav1 hasn't been built according to the README instructions.
|
||||
if (project.file('src/main/jni/libgav1').exists()) {
|
||||
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
|
||||
android.externalNativeBuild.cmake.version = '3.7.1+'
|
||||
android.externalNativeBuild.cmake {
|
||||
path = 'src/main/jni/CMakeLists.txt'
|
||||
version = '3.7.1+'
|
||||
if (project.hasProperty('externalNativeBuildDir')) {
|
||||
if (!new File(externalNativeBuildDir).isAbsolute()) {
|
||||
ext.externalNativeBuildDir =
|
||||
new File(rootDir, it.externalNativeBuildDir)
|
||||
}
|
||||
buildStagingDirectory = "${externalNativeBuildDir}/${project.name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -20,6 +20,7 @@ import static java.lang.Runtime.getRuntime;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
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.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
|
||||
@ -83,18 +84,9 @@ import java.nio.ByteBuffer;
|
||||
return "libgav1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the output mode for frames rendered by the decoder.
|
||||
*
|
||||
* @param outputMode The output mode.
|
||||
*/
|
||||
public void setOutputMode(@C.VideoOutputMode int outputMode) {
|
||||
this.outputMode = outputMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VideoDecoderInputBuffer createInputBuffer() {
|
||||
return new VideoDecoderInputBuffer();
|
||||
return new VideoDecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -102,8 +94,8 @@ import java.nio.ByteBuffer;
|
||||
return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@Nullable
|
||||
protected Gav1DecoderException decode(
|
||||
VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
|
||||
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
|
||||
@ -128,7 +120,7 @@ import java.nio.ByteBuffer;
|
||||
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
}
|
||||
if (!decodeOnly) {
|
||||
outputBuffer.colorInfo = inputBuffer.colorInfo;
|
||||
outputBuffer.format = inputBuffer.format;
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -155,6 +147,15 @@ import java.nio.ByteBuffer;
|
||||
super.releaseOutputBuffer(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the output mode for frames rendered by the decoder.
|
||||
*
|
||||
* @param outputMode The output mode.
|
||||
*/
|
||||
public void setOutputMode(@C.VideoOutputMode int outputMode) {
|
||||
this.outputMode = outputMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders output buffer to the given surface. Must only be called when in {@link
|
||||
* C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
|
||||
@ -217,7 +218,7 @@ import java.nio.ByteBuffer;
|
||||
* @param context Decoder context.
|
||||
* @param surface Output surface.
|
||||
* @param outputBuffer Output buffer with the decoded frame.
|
||||
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occured.
|
||||
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occurred.
|
||||
*/
|
||||
private native int gav1RenderFrame(
|
||||
long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
|
||||
@ -239,10 +240,10 @@ import java.nio.ByteBuffer;
|
||||
private native String gav1GetErrorMessage(long context);
|
||||
|
||||
/**
|
||||
* Returns whether an error occured.
|
||||
* Returns whether an error occurred.
|
||||
*
|
||||
* @param context Decoder context.
|
||||
* @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured.
|
||||
* @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occurred.
|
||||
*/
|
||||
private native int gav1CheckError(long context);
|
||||
|
||||
|
@ -15,10 +15,10 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.av1;
|
||||
|
||||
import com.google.android.exoplayer2.video.VideoDecoderException;
|
||||
import com.google.android.exoplayer2.decoder.DecoderException;
|
||||
|
||||
/** Thrown when a libgav1 decoder error occurs. */
|
||||
public final class Gav1DecoderException extends VideoDecoderException {
|
||||
public final class Gav1DecoderException extends DecoderException {
|
||||
|
||||
/* package */ Gav1DecoderException(String message) {
|
||||
super(message);
|
||||
|
@ -19,39 +19,18 @@ import android.os.Handler;
|
||||
import android.view.Surface;
|
||||
import androidx.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.PlayerMessage.Target;
|
||||
import com.google.android.exoplayer2.RendererCapabilities;
|
||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderException;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.video.DecoderVideoRenderer;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
|
||||
import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||
|
||||
/**
|
||||
* Decodes and renders video using libgav1 decoder.
|
||||
*
|
||||
* <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
|
||||
* on the playback thread:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload
|
||||
* should be the target {@link Surface}, or null.
|
||||
* <li>Message with type {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output
|
||||
* buffer renderer. The message payload should be the target {@link
|
||||
* VideoDecoderOutputBufferRenderer}, or null.
|
||||
* </ul>
|
||||
*/
|
||||
public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||
/** Decodes and renders video using libgav1 decoder. */
|
||||
public class Libgav1VideoRenderer extends DecoderVideoRenderer {
|
||||
|
||||
/**
|
||||
* Attempts to use as many threads as performance processors available on the device. If the
|
||||
@ -60,6 +39,7 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||
*/
|
||||
public static final int THREAD_COUNT_AUTODETECT = 0;
|
||||
|
||||
private static final String TAG = "Libgav1VideoRenderer";
|
||||
private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4;
|
||||
private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4;
|
||||
/* Default size based on 720p resolution video compressed by a factor of two. */
|
||||
@ -79,7 +59,7 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||
@Nullable private Gav1Decoder decoder;
|
||||
|
||||
/**
|
||||
* Creates a Libgav1VideoRenderer.
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
@ -105,7 +85,7 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Libgav1VideoRenderer.
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||
* can attempt to seamlessly join an ongoing playback.
|
||||
@ -114,9 +94,9 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
||||
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||
* @param threads Number of threads libgav1 will use to decode. If
|
||||
* {@link #THREAD_COUNT_AUTODETECT} is passed, then the number of threads to use is
|
||||
* auto-detected based on CPU capabilities.
|
||||
* @param threads Number of threads libgav1 will use to decode. If {@link
|
||||
* #THREAD_COUNT_AUTODETECT} is passed, then the number of threads to use is autodetected
|
||||
* based on CPU capabilities.
|
||||
* @param numInputBuffers Number of input buffers.
|
||||
* @param numOutputBuffers Number of output buffers.
|
||||
*/
|
||||
@ -128,39 +108,33 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||
int threads,
|
||||
int numInputBuffers,
|
||||
int numOutputBuffers) {
|
||||
super(
|
||||
allowedJoiningTimeMs,
|
||||
eventHandler,
|
||||
eventListener,
|
||||
maxDroppedFramesToNotify,
|
||||
/* drmSessionManager= */ null,
|
||||
/* playClearSamplesWithoutKeys= */ false);
|
||||
super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify);
|
||||
this.threads = threads;
|
||||
this.numInputBuffers = numInputBuffers;
|
||||
this.numOutputBuffers = numOutputBuffers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Capabilities
|
||||
protected int supportsFormatInternal(
|
||||
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
|
||||
public final int supportsFormat(Format format) {
|
||||
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|
||||
|| !Gav1Library.isAvailable()) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
|
||||
}
|
||||
if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
if (format.exoMediaCryptoType != null) {
|
||||
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
|
||||
}
|
||||
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SimpleDecoder<
|
||||
VideoDecoderInputBuffer,
|
||||
? extends VideoDecoderOutputBuffer,
|
||||
? extends VideoDecoderException>
|
||||
createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||
throws VideoDecoderException {
|
||||
protected Gav1Decoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||
throws Gav1DecoderException {
|
||||
TraceUtil.beginSection("createGav1Decoder");
|
||||
int initialInputBufferSize =
|
||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||
@ -189,16 +163,8 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// PlayerMessage.Target implementation.
|
||||
|
||||
@Override
|
||||
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
|
||||
if (messageType == C.MSG_SET_SURFACE) {
|
||||
setOutputSurface((Surface) message);
|
||||
} else if (messageType == C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) {
|
||||
setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message);
|
||||
} else {
|
||||
super.handleMessage(messageType, message);
|
||||
}
|
||||
protected boolean canKeepCodec(Format oldFormat, Format newFormat) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
# libgav1JNI requires modern CMake.
|
||||
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
|
||||
|
||||
# libgav1JNI requires C++11.
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
project(libgav1JNI C CXX)
|
||||
@ -21,24 +18,13 @@ if(build_type MATCHES "^rel")
|
||||
endif()
|
||||
|
||||
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
|
||||
set(libgav1_jni_output_directory
|
||||
${libgav1_jni_root}/../libs/${ANDROID_ABI}/)
|
||||
|
||||
set(libgav1_root "${libgav1_jni_root}/libgav1")
|
||||
set(libgav1_build "${libgav1_jni_build}/libgav1")
|
||||
|
||||
set(cpu_features_root "${libgav1_jni_root}/cpu_features")
|
||||
set(cpu_features_build "${libgav1_jni_build}/cpu_features")
|
||||
|
||||
# Build cpu_features library.
|
||||
add_subdirectory("${cpu_features_root}"
|
||||
"${cpu_features_build}"
|
||||
add_subdirectory("${libgav1_jni_root}/cpu_features"
|
||||
EXCLUDE_FROM_ALL)
|
||||
|
||||
# Build libgav1.
|
||||
add_subdirectory("${libgav1_root}"
|
||||
"${libgav1_build}"
|
||||
add_subdirectory("${libgav1_jni_root}/libgav1"
|
||||
EXCLUDE_FROM_ALL)
|
||||
|
||||
# Build libgav1JNI.
|
||||
@ -58,7 +44,3 @@ target_link_libraries(gav1JNI
|
||||
PRIVATE libgav1_static
|
||||
PRIVATE ${android_log_lib})
|
||||
|
||||
# Specify output directory for libgav1JNI.
|
||||
set_target_properties(gav1JNI PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY
|
||||
${libgav1_jni_output_directory})
|
||||
|
@ -73,7 +73,7 @@ const int kImageFormatYV12 = 0x32315659;
|
||||
// Output modes.
|
||||
const int kOutputModeYuv = 0;
|
||||
const int kOutputModeSurfaceYuv = 1;
|
||||
// LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/C.java)
|
||||
// LINT.ThenChange(../../../../../library/common/src/main/java/com/google/android/exoplayer2/C.java)
|
||||
|
||||
// LINT.IfChange
|
||||
const int kColorSpaceUnknown = 0;
|
||||
|
@ -11,24 +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 plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
dependencies {
|
||||
api 'com.google.android.gms:play-services-cast-framework:18.1.0'
|
||||
@ -36,7 +19,8 @@ dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
@ -15,12 +15,15 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.BasePlayer;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
@ -29,6 +32,7 @@ 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.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
@ -83,6 +87,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
|
||||
|
||||
private final CastContext castContext;
|
||||
private final MediaItemConverter mediaItemConverter;
|
||||
// TODO: Allow custom implementations of CastTimelineTracker.
|
||||
private final CastTimelineTracker timelineTracker;
|
||||
private final Timeline.Period period;
|
||||
@ -110,13 +115,25 @@ public final class CastPlayer extends BasePlayer {
|
||||
private int pendingSeekCount;
|
||||
private int pendingSeekWindowIndex;
|
||||
private long pendingSeekPositionMs;
|
||||
private boolean waitingForInitialTimeline;
|
||||
|
||||
/**
|
||||
* Creates a new cast player that uses a {@link DefaultMediaItemConverter}.
|
||||
*
|
||||
* @param castContext The context from which the cast session is obtained.
|
||||
*/
|
||||
public CastPlayer(CastContext castContext) {
|
||||
this(castContext, new DefaultMediaItemConverter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new cast player.
|
||||
*
|
||||
* @param castContext The context from which the cast session is obtained.
|
||||
* @param mediaItemConverter The {@link MediaItemConverter} to use.
|
||||
*/
|
||||
public CastPlayer(CastContext castContext, MediaItemConverter mediaItemConverter) {
|
||||
this.castContext = castContext;
|
||||
this.mediaItemConverter = mediaItemConverter;
|
||||
timelineTracker = new CastTimelineTracker();
|
||||
period = new Timeline.Period();
|
||||
statusListener = new StatusListener();
|
||||
@ -143,106 +160,61 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
// Media Queue manipulation methods.
|
||||
|
||||
/**
|
||||
* Loads a single item media queue. If no session is available, does nothing.
|
||||
*
|
||||
* @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}. If {@link C#TIME_UNSET} is passed, playback
|
||||
* starts at position 0.
|
||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||
*/
|
||||
/** @deprecated Use {@link #setMediaItems(List, int, long)} instead. */
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
|
||||
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
|
||||
return setMediaItemsInternal(
|
||||
new MediaQueueItem[] {item}, /* startWindowIndex= */ 0, positionMs, repeatMode.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}. 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.
|
||||
* @deprecated Use {@link #setMediaItems(List, int, long)} and {@link #setRepeatMode(int)}
|
||||
* instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> 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);
|
||||
}
|
||||
return null;
|
||||
return setMediaItemsInternal(items, startIndex, positionMs, repeatMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/** @deprecated Use {@link #addMediaItems(List)} instead. */
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
|
||||
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
|
||||
return addMediaItemsInternal(items, MediaQueueItem.INVALID_ITEM_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ({@link #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.
|
||||
*/
|
||||
/** @deprecated Use {@link #addMediaItems(int, List)} instead. */
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> 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);
|
||||
if (periodId == MediaQueueItem.INVALID_ITEM_ID
|
||||
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
return addMediaItemsInternal(items, periodId);
|
||||
}
|
||||
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 ({@link #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.
|
||||
*/
|
||||
/** @deprecated Use {@link #removeMediaItem(int)} instead. */
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> removeItem(int periodId) {
|
||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
return remoteMediaClient.queueRemoveItem(periodId, null);
|
||||
if (currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||
return removeMediaItemsInternal(new int[] {periodId});
|
||||
}
|
||||
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 ({@link #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.
|
||||
*/
|
||||
/** @deprecated Use {@link #moveMediaItem(int, int)} instead. */
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public PendingResult<MediaChannelResult> 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);
|
||||
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getWindowCount());
|
||||
int fromIndex = currentTimeline.getIndexOfPeriod(periodId);
|
||||
if (fromIndex != C.INDEX_UNSET && fromIndex != newIndex) {
|
||||
return moveMediaItemsInternal(new int[] {periodId}, fromIndex, newIndex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -307,6 +279,13 @@ public final class CastPlayer extends BasePlayer {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public DeviceComponent getDeviceComponent() {
|
||||
// TODO(b/151792305): Implement the component.
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Looper getApplicationLooper() {
|
||||
return Looper.getMainLooper();
|
||||
@ -314,6 +293,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
@Override
|
||||
public void addListener(EventListener listener) {
|
||||
Assertions.checkNotNull(listener);
|
||||
listeners.addIfAbsent(new ListenerHolder(listener));
|
||||
}
|
||||
|
||||
@ -327,6 +307,73 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaItems(
|
||||
List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs) {
|
||||
setMediaItemsInternal(
|
||||
toMediaQueueItems(mediaItems), startWindowIndex, startPositionMs, repeatMode.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMediaItems(List<MediaItem> mediaItems) {
|
||||
addMediaItemsInternal(toMediaQueueItems(mediaItems), MediaQueueItem.INVALID_ITEM_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMediaItems(int index, List<MediaItem> mediaItems) {
|
||||
Assertions.checkArgument(index >= 0);
|
||||
int uid = MediaQueueItem.INVALID_ITEM_ID;
|
||||
if (index < currentTimeline.getWindowCount()) {
|
||||
uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
|
||||
}
|
||||
addMediaItemsInternal(toMediaQueueItems(mediaItems), uid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
|
||||
Assertions.checkArgument(
|
||||
fromIndex >= 0
|
||||
&& fromIndex <= toIndex
|
||||
&& toIndex <= currentTimeline.getWindowCount()
|
||||
&& newIndex >= 0
|
||||
&& newIndex < currentTimeline.getWindowCount());
|
||||
newIndex = min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex));
|
||||
if (fromIndex == toIndex || fromIndex == newIndex) {
|
||||
// Do nothing.
|
||||
return;
|
||||
}
|
||||
int[] uids = new int[toIndex - fromIndex];
|
||||
for (int i = 0; i < uids.length; i++) {
|
||||
uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid;
|
||||
}
|
||||
moveMediaItemsInternal(uids, fromIndex, newIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMediaItems(int fromIndex, int toIndex) {
|
||||
Assertions.checkArgument(
|
||||
fromIndex >= 0 && toIndex >= fromIndex && toIndex <= currentTimeline.getWindowCount());
|
||||
if (fromIndex == toIndex) {
|
||||
// Do nothing.
|
||||
return;
|
||||
}
|
||||
int[] uids = new int[toIndex - fromIndex];
|
||||
for (int i = 0; i < uids.length; i++) {
|
||||
uids[i] = (int) currentTimeline.getWindow(/* windowIndex= */ i + fromIndex, window).uid;
|
||||
}
|
||||
removeMediaItemsInternal(uids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearMediaItems() {
|
||||
removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ currentTimeline.getWindowCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
@Player.State
|
||||
public int getPlaybackState() {
|
||||
@ -339,9 +386,16 @@ public final class CastPlayer extends BasePlayer {
|
||||
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
@Nullable
|
||||
public ExoPlaybackException getPlaybackError() {
|
||||
return getPlayerError();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ExoPlaybackException getPlayerError() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -353,7 +407,8 @@ public final class CastPlayer extends BasePlayer {
|
||||
// We update the local state and send the message to the receiver app, which will cause the
|
||||
// operation to be perceived as synchronous by the user. When the operation reports a result,
|
||||
// the local state will be updated to reflect the state reported by the Cast SDK.
|
||||
setPlayerStateAndNotifyIfChanged(playWhenReady, playbackState);
|
||||
setPlayerStateAndNotifyIfChanged(
|
||||
playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, playbackState);
|
||||
flushNotifications();
|
||||
PendingResult<MediaChannelResult> pendingResult =
|
||||
playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
|
||||
@ -375,6 +430,9 @@ public final class CastPlayer extends BasePlayer {
|
||||
return playWhenReady.value;
|
||||
}
|
||||
|
||||
// We still call EventListener#onSeekProcessed() for backwards compatibility with listeners that
|
||||
// don't implement onPositionDiscontinuity().
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void seekTo(int windowIndex, long positionMs) {
|
||||
MediaStatus mediaStatus = getMediaStatus();
|
||||
@ -446,6 +504,12 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public TrackSelector getTrackSelector() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRepeatMode(@RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient == null) {
|
||||
@ -627,8 +691,14 @@ public final class CastPlayer extends BasePlayer {
|
||||
newPlayWhenReadyValue = !remoteMediaClient.isPaused();
|
||||
playWhenReady.clearPendingResultCallback();
|
||||
}
|
||||
@PlayWhenReadyChangeReason
|
||||
int playWhenReadyChangeReason =
|
||||
newPlayWhenReadyValue != playWhenReady.value
|
||||
? PLAY_WHEN_READY_CHANGE_REASON_REMOTE
|
||||
: PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
|
||||
// We do not mask the playback state, so try setting it regardless of the playWhenReady masking.
|
||||
setPlayerStateAndNotifyIfChanged(newPlayWhenReadyValue, fetchPlaybackState(remoteMediaClient));
|
||||
setPlayerStateAndNotifyIfChanged(
|
||||
newPlayWhenReadyValue, playWhenReadyChangeReason, fetchPlaybackState(remoteMediaClient));
|
||||
}
|
||||
|
||||
@RequiresNonNull("remoteMediaClient")
|
||||
@ -641,15 +711,13 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
private void updateTimelineAndNotifyIfChanged() {
|
||||
if (updateTimeline()) {
|
||||
@Player.TimelineChangeReason
|
||||
int reason =
|
||||
waitingForInitialTimeline
|
||||
? Player.TIMELINE_CHANGE_REASON_PREPARED
|
||||
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
||||
waitingForInitialTimeline = false;
|
||||
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
|
||||
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onTimelineChanged(currentTimeline, reason)));
|
||||
listener ->
|
||||
listener.onTimelineChanged(
|
||||
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -713,6 +781,58 @@ public final class CastPlayer extends BasePlayer {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private PendingResult<MediaChannelResult> setMediaItemsInternal(
|
||||
MediaQueueItem[] mediaQueueItems,
|
||||
int startWindowIndex,
|
||||
long startPositionMs,
|
||||
@RepeatMode int repeatMode) {
|
||||
if (remoteMediaClient == null || mediaQueueItems.length == 0) {
|
||||
return null;
|
||||
}
|
||||
startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs;
|
||||
if (startWindowIndex == C.INDEX_UNSET) {
|
||||
startWindowIndex = getCurrentWindowIndex();
|
||||
startPositionMs = getCurrentPosition();
|
||||
}
|
||||
return remoteMediaClient.queueLoad(
|
||||
mediaQueueItems,
|
||||
min(startWindowIndex, mediaQueueItems.length - 1),
|
||||
getCastRepeatMode(repeatMode),
|
||||
startPositionMs,
|
||||
/* customData= */ null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private PendingResult<MediaChannelResult> addMediaItemsInternal(MediaQueueItem[] items, int uid) {
|
||||
if (remoteMediaClient == null || getMediaStatus() == null) {
|
||||
return null;
|
||||
}
|
||||
return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private PendingResult<MediaChannelResult> moveMediaItemsInternal(
|
||||
int[] uids, int fromIndex, int newIndex) {
|
||||
if (remoteMediaClient == null || getMediaStatus() == null) {
|
||||
return null;
|
||||
}
|
||||
int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex;
|
||||
int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID;
|
||||
if (insertBeforeIndex < currentTimeline.getWindowCount()) {
|
||||
insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid;
|
||||
}
|
||||
return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private PendingResult<MediaChannelResult> removeMediaItemsInternal(int[] uids) {
|
||||
if (remoteMediaClient == null || getMediaStatus() == null) {
|
||||
return null;
|
||||
}
|
||||
return remoteMediaClient.queueRemoveItems(uids, /* customData= */ null);
|
||||
}
|
||||
|
||||
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
|
||||
if (this.repeatMode.value != repeatMode) {
|
||||
this.repeatMode.value = repeatMode;
|
||||
@ -721,14 +841,27 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void setPlayerStateAndNotifyIfChanged(
|
||||
boolean playWhenReady, @Player.State int playbackState) {
|
||||
if (this.playWhenReady.value != playWhenReady || this.playbackState != playbackState) {
|
||||
this.playWhenReady.value = playWhenReady;
|
||||
boolean playWhenReady,
|
||||
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
|
||||
@Player.State int playbackState) {
|
||||
boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady;
|
||||
boolean playbackStateChanged = this.playbackState != playbackState;
|
||||
if (playWhenReadyChanged || playbackStateChanged) {
|
||||
this.playbackState = playbackState;
|
||||
this.playWhenReady.value = playWhenReady;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onPlayerStateChanged(playWhenReady, playbackState)));
|
||||
listener -> {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||
if (playbackStateChanged) {
|
||||
listener.onPlaybackStateChanged(playbackState);
|
||||
}
|
||||
if (playWhenReadyChanged) {
|
||||
listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -738,7 +871,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
return;
|
||||
}
|
||||
if (this.remoteMediaClient != null) {
|
||||
this.remoteMediaClient.removeListener(statusListener);
|
||||
this.remoteMediaClient.unregisterCallback(statusListener);
|
||||
this.remoteMediaClient.removeProgressListener(statusListener);
|
||||
}
|
||||
this.remoteMediaClient = remoteMediaClient;
|
||||
@ -746,10 +879,11 @@ public final class CastPlayer extends BasePlayer {
|
||||
if (sessionAvailabilityListener != null) {
|
||||
sessionAvailabilityListener.onCastSessionAvailable();
|
||||
}
|
||||
remoteMediaClient.addListener(statusListener);
|
||||
remoteMediaClient.registerCallback(statusListener);
|
||||
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
|
||||
updateInternalStateAndNotifyIfChanged();
|
||||
} else {
|
||||
updateTimelineAndNotifyIfChanged();
|
||||
if (sessionAvailabilityListener != null) {
|
||||
sessionAvailabilityListener.onCastSessionUnavailable();
|
||||
}
|
||||
@ -849,12 +983,18 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
private MediaQueueItem[] toMediaQueueItems(List<MediaItem> mediaItems) {
|
||||
MediaQueueItem[] mediaQueueItems = new MediaQueueItem[mediaItems.size()];
|
||||
for (int i = 0; i < mediaItems.size(); i++) {
|
||||
mediaQueueItems[i] = mediaItemConverter.toMediaQueueItem(mediaItems.get(i));
|
||||
}
|
||||
return mediaQueueItems;
|
||||
}
|
||||
|
||||
// Internal classes.
|
||||
|
||||
private final class StatusListener
|
||||
implements RemoteMediaClient.Listener,
|
||||
SessionManagerListener<CastSession>,
|
||||
RemoteMediaClient.ProgressListener {
|
||||
private final class StatusListener extends RemoteMediaClient.Callback
|
||||
implements SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
|
||||
|
||||
// RemoteMediaClient.ProgressListener implementation.
|
||||
|
||||
@ -863,7 +1003,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
lastReportedPositionMs = progressMs;
|
||||
}
|
||||
|
||||
// RemoteMediaClient.Listener implementation.
|
||||
// RemoteMediaClient.Callback implementation.
|
||||
|
||||
@Override
|
||||
public void onStatusUpdated() {
|
||||
@ -940,6 +1080,9 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||
|
||||
// We still call EventListener#onSeekProcessed() for backwards compatibility with listeners that
|
||||
// don't implement onPositionDiscontinuity().
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void onResult(MediaChannelResult result) {
|
||||
int statusCode = result.getStatus().getStatusCode();
|
||||
|
@ -15,10 +15,12 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseIntArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -126,10 +128,11 @@ import java.util.Arrays;
|
||||
boolean isDynamic = durationUs == C.TIME_UNSET;
|
||||
return window.set(
|
||||
/* uid= */ ids[windowIndex],
|
||||
/* tag= */ ids[windowIndex],
|
||||
/* mediaItem= */ new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build(),
|
||||
/* manifest= */ null,
|
||||
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
||||
/* windowStartTimeMs= */ C.TIME_UNSET,
|
||||
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
|
||||
/* isSeekable= */ !isDynamic,
|
||||
isDynamic,
|
||||
isLive[windowIndex],
|
||||
|
@ -104,16 +104,11 @@ import com.google.android.gms.cast.MediaTrack;
|
||||
* @return The equivalent {@link Format}.
|
||||
*/
|
||||
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
|
||||
return Format.createContainerFormat(
|
||||
mediaTrack.getContentId(),
|
||||
/* label= */ null,
|
||||
mediaTrack.getContentType(),
|
||||
/* sampleMimeType= */ null,
|
||||
/* codecs= */ null,
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
/* selectionFlags= */ 0,
|
||||
/* roleFlags= */ 0,
|
||||
mediaTrack.getLanguage());
|
||||
return new Format.Builder()
|
||||
.setId(mediaTrack.getContentId())
|
||||
.setContainerMimeType(mediaTrack.getContentType())
|
||||
.setLanguage(mediaTrack.getLanguage())
|
||||
.build();
|
||||
}
|
||||
|
||||
private CastUtils() {}
|
||||
|
@ -18,7 +18,8 @@ package com.google.android.exoplayer2.ext.cast;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaMetadata;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
@ -43,22 +44,24 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||
|
||||
@Override
|
||||
public MediaItem toMediaItem(MediaQueueItem item) {
|
||||
return getMediaItem(item.getMedia().getCustomData());
|
||||
// `item` came from `toMediaQueueItem()` so the custom JSON data must be set.
|
||||
return getMediaItem(Assertions.checkNotNull(item.getMedia().getCustomData()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaQueueItem toMediaQueueItem(MediaItem item) {
|
||||
if (item.mimeType == null) {
|
||||
Assertions.checkNotNull(item.playbackProperties);
|
||||
if (item.playbackProperties.mimeType == null) {
|
||||
throw new IllegalArgumentException("The item must specify its mimeType");
|
||||
}
|
||||
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||
if (item.title != null) {
|
||||
metadata.putString(MediaMetadata.KEY_TITLE, item.title);
|
||||
if (item.mediaMetadata.title != null) {
|
||||
metadata.putString(MediaMetadata.KEY_TITLE, item.mediaMetadata.title);
|
||||
}
|
||||
MediaInfo mediaInfo =
|
||||
new MediaInfo.Builder(item.uri.toString())
|
||||
new MediaInfo.Builder(item.playbackProperties.uri.toString())
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setContentType(item.mimeType)
|
||||
.setContentType(item.playbackProperties.mimeType)
|
||||
.setMetadata(metadata)
|
||||
.setCustomData(getCustomData(item))
|
||||
.build();
|
||||
@ -73,14 +76,17 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
|
||||
if (mediaItemJson.has(KEY_TITLE)) {
|
||||
builder.setTitle(mediaItemJson.getString(KEY_TITLE));
|
||||
com.google.android.exoplayer2.MediaMetadata mediaMetadata =
|
||||
new com.google.android.exoplayer2.MediaMetadata.Builder()
|
||||
.setTitle(mediaItemJson.getString(KEY_TITLE))
|
||||
.build();
|
||||
builder.setMediaMetadata(mediaMetadata);
|
||||
}
|
||||
if (mediaItemJson.has(KEY_MIME_TYPE)) {
|
||||
builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
|
||||
}
|
||||
if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) {
|
||||
builder.setDrmConfiguration(
|
||||
getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION)));
|
||||
populateDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION), builder);
|
||||
}
|
||||
return builder.build();
|
||||
} catch (JSONException e) {
|
||||
@ -88,25 +94,26 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||
}
|
||||
}
|
||||
|
||||
private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException {
|
||||
UUID uuid = UUID.fromString(json.getString(KEY_UUID));
|
||||
Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI));
|
||||
private static void populateDrmConfiguration(JSONObject json, MediaItem.Builder builder)
|
||||
throws JSONException {
|
||||
builder.setDrmUuid(UUID.fromString(json.getString(KEY_UUID)));
|
||||
builder.setDrmLicenseUri(json.getString(KEY_LICENSE_URI));
|
||||
JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS);
|
||||
HashMap<String, String> requestHeaders = new HashMap<>();
|
||||
for (Iterator<String> iterator = requestHeadersJson.keys(); iterator.hasNext(); ) {
|
||||
String key = iterator.next();
|
||||
requestHeaders.put(key, requestHeadersJson.getString(key));
|
||||
}
|
||||
return new DrmConfiguration(uuid, licenseUri, requestHeaders);
|
||||
builder.setDrmLicenseRequestHeaders(requestHeaders);
|
||||
}
|
||||
|
||||
// Serialization.
|
||||
|
||||
private static JSONObject getCustomData(MediaItem item) {
|
||||
private static JSONObject getCustomData(MediaItem mediaItem) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put(KEY_MEDIA_ITEM, getMediaItemJson(item));
|
||||
JSONObject playerConfigJson = getPlayerConfigJson(item);
|
||||
json.put(KEY_MEDIA_ITEM, getMediaItemJson(mediaItem));
|
||||
@Nullable JSONObject playerConfigJson = getPlayerConfigJson(mediaItem);
|
||||
if (playerConfigJson != null) {
|
||||
json.put(KEY_PLAYER_CONFIG, playerConfigJson);
|
||||
}
|
||||
@ -116,18 +123,21 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||
return json;
|
||||
}
|
||||
|
||||
private static JSONObject getMediaItemJson(MediaItem item) throws JSONException {
|
||||
private static JSONObject getMediaItemJson(MediaItem mediaItem) throws JSONException {
|
||||
Assertions.checkNotNull(mediaItem.playbackProperties);
|
||||
JSONObject json = new JSONObject();
|
||||
json.put(KEY_URI, item.uri.toString());
|
||||
json.put(KEY_TITLE, item.title);
|
||||
json.put(KEY_MIME_TYPE, item.mimeType);
|
||||
if (item.drmConfiguration != null) {
|
||||
json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration));
|
||||
json.put(KEY_TITLE, mediaItem.mediaMetadata.title);
|
||||
json.put(KEY_URI, mediaItem.playbackProperties.uri.toString());
|
||||
json.put(KEY_MIME_TYPE, mediaItem.playbackProperties.mimeType);
|
||||
if (mediaItem.playbackProperties.drmConfiguration != null) {
|
||||
json.put(
|
||||
KEY_DRM_CONFIGURATION,
|
||||
getDrmConfigurationJson(mediaItem.playbackProperties.drmConfiguration));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration)
|
||||
private static JSONObject getDrmConfigurationJson(MediaItem.DrmConfiguration drmConfiguration)
|
||||
throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put(KEY_UUID, drmConfiguration.uuid);
|
||||
@ -137,11 +147,12 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException {
|
||||
DrmConfiguration drmConfiguration = item.drmConfiguration;
|
||||
if (drmConfiguration == null) {
|
||||
private static JSONObject getPlayerConfigJson(MediaItem mediaItem) throws JSONException {
|
||||
if (mediaItem.playbackProperties == null
|
||||
|| mediaItem.playbackProperties.drmConfiguration == null) {
|
||||
return null;
|
||||
}
|
||||
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
|
||||
|
||||
String drmScheme;
|
||||
if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) {
|
||||
|
@ -1,175 +0,0 @@
|
||||
/*
|
||||
* 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.ext.cast;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Representation of a media item. */
|
||||
public final class MediaItem {
|
||||
|
||||
/** A builder for {@link MediaItem} instances. */
|
||||
public static final class Builder {
|
||||
|
||||
@Nullable private Uri uri;
|
||||
@Nullable private String title;
|
||||
@Nullable private String mimeType;
|
||||
@Nullable private DrmConfiguration drmConfiguration;
|
||||
|
||||
/** See {@link MediaItem#uri}. */
|
||||
public Builder setUri(String uri) {
|
||||
return setUri(Uri.parse(uri));
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#uri}. */
|
||||
public Builder setUri(Uri uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#title}. */
|
||||
public Builder setTitle(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#mimeType}. */
|
||||
public Builder setMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link MediaItem#drmConfiguration}. */
|
||||
public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) {
|
||||
this.drmConfiguration = drmConfiguration;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns a new {@link MediaItem} instance with the current builder values. */
|
||||
public MediaItem build() {
|
||||
Assertions.checkNotNull(uri);
|
||||
return new MediaItem(uri, title, mimeType, drmConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/** DRM configuration for a media item. */
|
||||
public static final class DrmConfiguration {
|
||||
|
||||
/** The UUID of the protection scheme. */
|
||||
public final UUID uuid;
|
||||
|
||||
/**
|
||||
* Optional license server {@link Uri}. If {@code null} then the license server must be
|
||||
* specified by the media.
|
||||
*/
|
||||
@Nullable public final Uri licenseUri;
|
||||
|
||||
/** Headers that should be attached to any license requests. */
|
||||
public final Map<String, String> requestHeaders;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param uuid See {@link #uuid}.
|
||||
* @param licenseUri See {@link #licenseUri}.
|
||||
* @param requestHeaders See {@link #requestHeaders}.
|
||||
*/
|
||||
public DrmConfiguration(
|
||||
UUID uuid, @Nullable Uri licenseUri, @Nullable Map<String, String> requestHeaders) {
|
||||
this.uuid = uuid;
|
||||
this.licenseUri = licenseUri;
|
||||
this.requestHeaders =
|
||||
requestHeaders == null
|
||||
? Collections.emptyMap()
|
||||
: Collections.unmodifiableMap(requestHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DrmConfiguration other = (DrmConfiguration) obj;
|
||||
return uuid.equals(other.uuid)
|
||||
&& Util.areEqual(licenseUri, other.licenseUri)
|
||||
&& requestHeaders.equals(other.requestHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = uuid.hashCode();
|
||||
result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0);
|
||||
result = 31 * result + requestHeaders.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/** The media {@link Uri}. */
|
||||
public final Uri uri;
|
||||
|
||||
/** The title of the item, or {@code null} if unspecified. */
|
||||
@Nullable public final String title;
|
||||
|
||||
/** The mime type for the media, or {@code null} if unspecified. */
|
||||
@Nullable public final String mimeType;
|
||||
|
||||
/** Optional {@link DrmConfiguration} for the media. */
|
||||
@Nullable public final DrmConfiguration drmConfiguration;
|
||||
|
||||
private MediaItem(
|
||||
Uri uri,
|
||||
@Nullable String title,
|
||||
@Nullable String mimeType,
|
||||
@Nullable DrmConfiguration drmConfiguration) {
|
||||
this.uri = uri;
|
||||
this.title = title;
|
||||
this.mimeType = mimeType;
|
||||
this.drmConfiguration = drmConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MediaItem other = (MediaItem) obj;
|
||||
return uri.equals(other.uri)
|
||||
&& Util.areEqual(title, other.title)
|
||||
&& Util.areEqual(mimeType, other.mimeType)
|
||||
&& Util.areEqual(drmConfiguration, other.drmConfiguration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = uri.hashCode();
|
||||
result = 31 * result + (title == null ? 0 : title.hashCode());
|
||||
result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
|
||||
result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
|
||||
return result;
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
|
||||
/** Converts between {@link MediaItem} and the Cast SDK's {@link MediaQueueItem}. */
|
||||
|
@ -18,13 +18,22 @@ package com.google.android.exoplayer2.ext.cast;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
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.MimeTypes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.cast.framework.CastSession;
|
||||
@ -33,6 +42,9 @@ import com.google.android.gms.cast.framework.media.MediaQueue;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
import com.google.android.gms.common.api.PendingResult;
|
||||
import com.google.android.gms.common.api.ResultCallback;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@ -46,9 +58,12 @@ import org.mockito.Mockito;
|
||||
public class CastPlayerTest {
|
||||
|
||||
private CastPlayer castPlayer;
|
||||
private RemoteMediaClient.Listener remoteMediaClientListener;
|
||||
|
||||
private RemoteMediaClient.Callback remoteMediaClientCallback;
|
||||
|
||||
@Mock private RemoteMediaClient mockRemoteMediaClient;
|
||||
@Mock private MediaStatus mockMediaStatus;
|
||||
@Mock private MediaInfo mockMediaInfo;
|
||||
@Mock private MediaQueue mockMediaQueue;
|
||||
@Mock private CastContext mockCastContext;
|
||||
@Mock private SessionManager mockSessionManager;
|
||||
@ -60,8 +75,11 @@ public class CastPlayerTest {
|
||||
private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>>
|
||||
setResultCallbackArgumentCaptor;
|
||||
|
||||
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor;
|
||||
@Captor private ArgumentCaptor<RemoteMediaClient.Callback> callbackArgumentCaptor;
|
||||
|
||||
@Captor private ArgumentCaptor<MediaQueueItem[]> queueItemsArgumentCaptor;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Before
|
||||
public void setUp() {
|
||||
initMocks(this);
|
||||
@ -76,22 +94,25 @@ public class CastPlayerTest {
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
|
||||
castPlayer = new CastPlayer(mockCastContext);
|
||||
castPlayer.addListener(mockListener);
|
||||
verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture());
|
||||
remoteMediaClientListener = listenerArgumentCaptor.getValue();
|
||||
verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture());
|
||||
remoteMediaClientCallback = callbackArgumentCaptor.getValue();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void testSetPlayWhenReady_masksRemoteState() {
|
||||
public void setPlayWhenReady_masksRemoteState() {
|
||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
|
||||
castPlayer.setPlayWhenReady(true);
|
||||
castPlayer.play();
|
||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
verify(mockListener)
|
||||
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
|
||||
// There is a status update in the middle, which should be hidden by masking.
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
|
||||
// Upon result, the remoteMediaClient has updated its state according to the play() call.
|
||||
@ -102,35 +123,59 @@ public class CastPlayerTest {
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void testSetPlayWhenReadyMasking_updatesUponResultChange() {
|
||||
public void setPlayWhenReadyMasking_updatesUponResultChange() {
|
||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
|
||||
castPlayer.setPlayWhenReady(true);
|
||||
castPlayer.play();
|
||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
verify(mockListener)
|
||||
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
|
||||
// Upon result, the remote media client is still paused. The state should reflect that.
|
||||
setResultCallbackArgumentCaptor
|
||||
.getValue()
|
||||
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
|
||||
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
|
||||
verify(mockListener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void testPlayWhenReady_changesOnStatusUpdates() {
|
||||
public void setPlayWhenReady_correctChangeReasonOnPause() {
|
||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||
when(mockRemoteMediaClient.pause()).thenReturn(mockPendingResult);
|
||||
castPlayer.play();
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
verify(mockListener)
|
||||
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
|
||||
castPlayer.pause();
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
|
||||
verify(mockListener)
|
||||
.onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Test
|
||||
public void playWhenReady_changesOnStatusUpdates() {
|
||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||
verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
|
||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetRepeatMode_masksRemoteState() {
|
||||
public void setRepeatMode_masksRemoteState() {
|
||||
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
|
||||
|
||||
@ -141,7 +186,7 @@ public class CastPlayerTest {
|
||||
|
||||
// There is a status update in the middle, which should be hidden by masking.
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
|
||||
// Upon result, the mediaStatus now exposes the new repeat mode.
|
||||
@ -153,7 +198,7 @@ public class CastPlayerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetRepeatMode_updatesUponResultChange() {
|
||||
public void setRepeatMode_updatesUponResultChange() {
|
||||
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
|
||||
|
||||
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
|
||||
@ -163,7 +208,7 @@ public class CastPlayerTest {
|
||||
|
||||
// There is a status update in the middle, which should be hidden by masking.
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verifyNoMoreInteractions(mockListener);
|
||||
|
||||
// Upon result, the repeat mode is ALL. The state should reflect that.
|
||||
@ -175,11 +220,279 @@ public class CastPlayerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepeatMode_changesOnStatusUpdates() {
|
||||
public void repeatMode_changesOnStatusUpdates() {
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
|
||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
|
||||
remoteMediaClientListener.onStatusUpdated();
|
||||
remoteMediaClientCallback.onStatusUpdated();
|
||||
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
|
||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setMediaItems_callsRemoteMediaClient() {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
String uri1 = "http://www.google.com/video1";
|
||||
String uri2 = "http://www.google.com/video2";
|
||||
mediaItems.add(
|
||||
new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build());
|
||||
mediaItems.add(
|
||||
new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build());
|
||||
|
||||
castPlayer.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L);
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueLoad(queueItemsArgumentCaptor.capture(), eq(1), anyInt(), eq(2000L), any());
|
||||
MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue();
|
||||
assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri1);
|
||||
assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setMediaItems_doNotReset_callsRemoteMediaClient() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
String uri1 = "http://www.google.com/video1";
|
||||
String uri2 = "http://www.google.com/video2";
|
||||
mediaItems.add(builder.setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build());
|
||||
mediaItems.add(builder.setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build());
|
||||
int startWindowIndex = C.INDEX_UNSET;
|
||||
long startPositionMs = 2000L;
|
||||
|
||||
castPlayer.setMediaItems(mediaItems, startWindowIndex, startPositionMs);
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueLoad(queueItemsArgumentCaptor.capture(), eq(0), anyInt(), eq(0L), any());
|
||||
|
||||
MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue();
|
||||
assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri1);
|
||||
assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addMediaItems_callsRemoteMediaClient() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
String uri1 = "http://www.google.com/video1";
|
||||
String uri2 = "http://www.google.com/video2";
|
||||
mediaItems.add(builder.setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build());
|
||||
mediaItems.add(builder.setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build());
|
||||
|
||||
castPlayer.addMediaItems(mediaItems);
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueInsertItems(
|
||||
queueItemsArgumentCaptor.capture(), eq(MediaQueueItem.INVALID_ITEM_ID), any());
|
||||
|
||||
MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue();
|
||||
assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri1);
|
||||
assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Test
|
||||
public void addMediaItems_insertAtIndex_callsRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
String uri = "http://www.google.com/video3";
|
||||
MediaItem anotherMediaItem =
|
||||
new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build();
|
||||
|
||||
// Add another on position 1
|
||||
int index = 1;
|
||||
castPlayer.addMediaItems(index, Collections.singletonList(anotherMediaItem));
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueInsertItems(
|
||||
queueItemsArgumentCaptor.capture(),
|
||||
eq((int) mediaItems.get(index).playbackProperties.tag),
|
||||
any());
|
||||
|
||||
MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue();
|
||||
assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveMediaItem_callsRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 2);
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueReorderItems(new int[] {2}, /* insertBeforeItemId= */ 4, /* customData= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveMediaItem_toBegin_callsRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0);
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueReorderItems(new int[] {2}, /* insertBeforeItemId= */ 1, /* customData= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveMediaItem_toEnd_callsRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 4);
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueReorderItems(
|
||||
new int[] {2},
|
||||
/* insertBeforeItemId= */ MediaQueueItem.INVALID_ITEM_ID,
|
||||
/* customData= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveMediaItems_callsRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 3, /* newIndex= */ 1);
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueReorderItems(
|
||||
new int[] {1, 2, 3}, /* insertBeforeItemId= */ 5, /* customData= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveMediaItems_toBeginning_callsRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4, /* newIndex= */ 0);
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueReorderItems(
|
||||
new int[] {2, 3, 4}, /* insertBeforeItemId= */ 1, /* customData= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveMediaItems_toEnd_callsRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 3);
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueReorderItems(
|
||||
new int[] {1, 2},
|
||||
/* insertBeforeItemId= */ MediaQueueItem.INVALID_ITEM_ID,
|
||||
/* customData= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveMediaItems_noItems_doesNotCallRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 1, /* newIndex= */ 0);
|
||||
|
||||
verify(mockRemoteMediaClient, never()).queueReorderItems(any(), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveMediaItems_noMove_doesNotCallRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 1);
|
||||
|
||||
verify(mockRemoteMediaClient, never()).queueReorderItems(any(), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeMediaItems_callsRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 4);
|
||||
|
||||
verify(mockRemoteMediaClient).queueRemoveItems(new int[] {2, 3, 4}, /* customData= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearMediaItems_callsRemoteMediaClient() {
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
castPlayer.clearMediaItems();
|
||||
|
||||
verify(mockRemoteMediaClient)
|
||||
.queueRemoveItems(new int[] {1, 2, 3, 4, 5}, /* customData= */ null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Test
|
||||
public void addMediaItems_fillsTimeline() {
|
||||
Timeline.Window window = new Timeline.Window();
|
||||
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 5);
|
||||
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
|
||||
|
||||
fillTimeline(mediaItems, mediaQueueItemIds);
|
||||
|
||||
Timeline currentTimeline = castPlayer.getCurrentTimeline();
|
||||
for (int i = 0; i < mediaItems.size(); i++) {
|
||||
assertThat(currentTimeline.getWindow(/* windowIndex= */ i, window).uid)
|
||||
.isEqualTo(mediaItems.get(i).playbackProperties.tag);
|
||||
}
|
||||
}
|
||||
|
||||
private int[] createMediaQueueItemIds(int numberOfIds) {
|
||||
int[] mediaQueueItemIds = new int[numberOfIds];
|
||||
for (int i = 0; i < numberOfIds; i++) {
|
||||
mediaQueueItemIds[i] = i + 1;
|
||||
}
|
||||
return mediaQueueItemIds;
|
||||
}
|
||||
|
||||
private List<MediaItem> createMediaItems(int[] mediaQueueItemIds) {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
for (int mediaQueueItemId : mediaQueueItemIds) {
|
||||
MediaItem mediaItem =
|
||||
builder
|
||||
.setUri("http://www.google.com/video" + mediaQueueItemId)
|
||||
.setMimeType(MimeTypes.APPLICATION_MPD)
|
||||
.setTag(mediaQueueItemId)
|
||||
.build();
|
||||
mediaItems.add(mediaItem);
|
||||
}
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
private void fillTimeline(List<MediaItem> mediaItems, int[] mediaQueueItemIds) {
|
||||
Assertions.checkState(mediaItems.size() == mediaQueueItemIds.length);
|
||||
List<MediaQueueItem> queueItems = new ArrayList<>();
|
||||
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
||||
for (MediaItem mediaItem : mediaItems) {
|
||||
queueItems.add(converter.toMediaQueueItem(mediaItem));
|
||||
}
|
||||
|
||||
// Set up mocks to allow the player to update the timeline.
|
||||
when(mockMediaQueue.getItemIds()).thenReturn(mediaQueueItemIds);
|
||||
when(mockMediaStatus.getCurrentItemId()).thenReturn(1);
|
||||
when(mockMediaStatus.getMediaInfo()).thenReturn(mockMediaInfo);
|
||||
when(mockMediaInfo.getStreamType()).thenReturn(MediaInfo.STREAM_TYPE_NONE);
|
||||
when(mockMediaStatus.getQueueItems()).thenReturn(queueItems);
|
||||
|
||||
castPlayer.addMediaItems(mediaItems);
|
||||
// Call listener to update the timeline of the player.
|
||||
remoteMediaClientCallback.onQueueStatusUpdated();
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cast;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||
@ -39,7 +41,7 @@ public class CastTimelineTrackerTest {
|
||||
|
||||
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
||||
@Test
|
||||
public void testGetCastTimelinePersistsDuration() {
|
||||
public void getCastTimelinePersistsDuration() {
|
||||
CastTimelineTracker tracker = new CastTimelineTracker();
|
||||
|
||||
RemoteMediaClient remoteMediaClient =
|
||||
@ -105,18 +107,18 @@ public class CastTimelineTrackerTest {
|
||||
int[] itemIds, int currentItemId, long currentDurationMs) {
|
||||
RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
|
||||
MediaStatus status = Mockito.mock(MediaStatus.class);
|
||||
Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
|
||||
Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);
|
||||
Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
|
||||
Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);
|
||||
when(status.getQueueItems()).thenReturn(Collections.emptyList());
|
||||
when(remoteMediaClient.getMediaStatus()).thenReturn(status);
|
||||
when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
|
||||
when(status.getCurrentItemId()).thenReturn(currentItemId);
|
||||
MediaQueue mediaQueue = mockMediaQueue(itemIds);
|
||||
Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
|
||||
when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
|
||||
return remoteMediaClient;
|
||||
}
|
||||
|
||||
private static MediaQueue mockMediaQueue(int[] itemIds) {
|
||||
MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
|
||||
Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds);
|
||||
when(mediaQueue.getItemIds()).thenReturn(itemIds);
|
||||
return mediaQueue;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,9 @@ import static com.google.common.truth.Truth.assertThat;
|
||||
import android.net.Uri;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.MediaMetadata;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.gms.cast.MediaQueueItem;
|
||||
import java.util.Collections;
|
||||
import org.junit.Test;
|
||||
@ -33,7 +35,8 @@ public class DefaultMediaItemConverterTest {
|
||||
@Test
|
||||
public void serialize_deserialize_minimal() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
MediaItem item = builder.setUri(Uri.parse("http://example.com")).setMimeType("mime").build();
|
||||
MediaItem item =
|
||||
builder.setUri("http://example.com").setMimeType(MimeTypes.APPLICATION_MPD).build();
|
||||
|
||||
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
||||
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
|
||||
@ -48,13 +51,11 @@ public class DefaultMediaItemConverterTest {
|
||||
MediaItem item =
|
||||
builder
|
||||
.setUri(Uri.parse("http://example.com"))
|
||||
.setTitle("title")
|
||||
.setMimeType("mime")
|
||||
.setDrmConfiguration(
|
||||
new DrmConfiguration(
|
||||
C.WIDEVINE_UUID,
|
||||
Uri.parse("http://license.com"),
|
||||
Collections.singletonMap("key", "value")))
|
||||
.setMediaMetadata(new MediaMetadata.Builder().build())
|
||||
.setMimeType(MimeTypes.APPLICATION_MPD)
|
||||
.setDrmUuid(C.WIDEVINE_UUID)
|
||||
.setDrmLicenseUri("http://license.com")
|
||||
.setDrmLicenseRequestHeaders(Collections.singletonMap("key", "value"))
|
||||
.build();
|
||||
|
||||
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
||||
|
@ -1,86 +0,0 @@
|
||||
/*
|
||||
* 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.ext.cast;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.HashMap;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Test for {@link MediaItem}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MediaItemTest {
|
||||
|
||||
@Test
|
||||
public void buildMediaItem_doesNotChangeState() {
|
||||
MediaItem.Builder builder = new MediaItem.Builder();
|
||||
MediaItem item1 =
|
||||
builder
|
||||
.setUri(Uri.parse("http://example.com"))
|
||||
.setTitle("title")
|
||||
.setMimeType(MimeTypes.AUDIO_MP4)
|
||||
.build();
|
||||
MediaItem item2 = builder.build();
|
||||
assertThat(item1).isEqualTo(item2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_withEqualDrmSchemes_returnsTrue() {
|
||||
MediaItem.Builder builder1 = new MediaItem.Builder();
|
||||
MediaItem mediaItem1 =
|
||||
builder1
|
||||
.setUri(Uri.parse("www.google.com"))
|
||||
.setDrmConfiguration(buildDrmConfiguration(1))
|
||||
.build();
|
||||
MediaItem.Builder builder2 = new MediaItem.Builder();
|
||||
MediaItem mediaItem2 =
|
||||
builder2
|
||||
.setUri(Uri.parse("www.google.com"))
|
||||
.setDrmConfiguration(buildDrmConfiguration(1))
|
||||
.build();
|
||||
assertThat(mediaItem1).isEqualTo(mediaItem2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
|
||||
MediaItem.Builder builder1 = new MediaItem.Builder();
|
||||
MediaItem mediaItem1 =
|
||||
builder1
|
||||
.setUri(Uri.parse("www.google.com"))
|
||||
.setDrmConfiguration(buildDrmConfiguration(1))
|
||||
.build();
|
||||
MediaItem.Builder builder2 = new MediaItem.Builder();
|
||||
MediaItem mediaItem2 =
|
||||
builder2
|
||||
.setUri(Uri.parse("www.google.com"))
|
||||
.setDrmConfiguration(buildDrmConfiguration(2))
|
||||
.build();
|
||||
assertThat(mediaItem1).isNotEqualTo(mediaItem2);
|
||||
}
|
||||
|
||||
private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) {
|
||||
HashMap<String, String> requestHeaders = new HashMap<>();
|
||||
requestHeaders.put("key1", "value1");
|
||||
requestHeaders.put("key2", "value2" + seed);
|
||||
return new MediaItem.DrmConfiguration(
|
||||
C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders);
|
||||
}
|
||||
}
|
@ -11,30 +11,21 @@
|
||||
// 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
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
dependencies {
|
||||
api "com.google.android.gms:play-services-cronet:17.0.0"
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation ('com.google.guava:guava:' + guavaVersion) {
|
||||
exclude group: 'com.google.code.findbugs', module: 'jsr305'
|
||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
||||
}
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
testImplementation project(modulePrefix + 'library')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.chromium.net.UploadDataProvider;
|
||||
@ -40,7 +42,7 @@ import org.chromium.net.UploadDataSink;
|
||||
|
||||
@Override
|
||||
public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException {
|
||||
int readLength = Math.min(byteBuffer.remaining(), data.length - position);
|
||||
int readLength = min(byteBuffer.remaining(), data.length - position);
|
||||
byteBuffer.put(data, position, readLength);
|
||||
position += readLength;
|
||||
uploadDataSink.onReadSucceeded(false);
|
||||
|
@ -16,6 +16,8 @@
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
@ -30,12 +32,14 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Clock;
|
||||
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.base.Predicate;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -146,6 +150,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
private volatile long currentConnectTimeoutMs;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
@ -164,6 +170,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
@ -195,6 +203,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
@ -229,6 +239,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
@ -241,6 +253,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
|
||||
* #setContentTypePredicate(Predicate)}.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
@ -257,6 +270,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
@ -274,6 +289,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
|
||||
* RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Deprecated
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
@ -295,6 +311,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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
|
||||
@ -440,12 +458,28 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
|
||||
int responseCode = responseInfo.getHttpStatusCode();
|
||||
if (responseCode < 200 || responseCode > 299) {
|
||||
byte[] responseBody = Util.EMPTY_BYTE_ARRAY;
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
while (!readBuffer.hasRemaining()) {
|
||||
operation.close();
|
||||
readBuffer.clear();
|
||||
readInternal(readBuffer);
|
||||
if (finished) {
|
||||
break;
|
||||
}
|
||||
readBuffer.flip();
|
||||
int existingResponseBodyEnd = responseBody.length;
|
||||
responseBody = Arrays.copyOf(responseBody, responseBody.length + readBuffer.remaining());
|
||||
readBuffer.get(responseBody, existingResponseBodyEnd, readBuffer.remaining());
|
||||
}
|
||||
|
||||
InvalidResponseCodeException exception =
|
||||
new InvalidResponseCodeException(
|
||||
responseCode,
|
||||
responseInfo.getHttpStatusText(),
|
||||
responseInfo.getAllHeaders(),
|
||||
dataSpec);
|
||||
dataSpec,
|
||||
responseBody);
|
||||
if (responseCode == 416) {
|
||||
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
|
||||
}
|
||||
@ -457,7 +491,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
if (contentTypePredicate != null) {
|
||||
List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
|
||||
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
|
||||
if (contentType != null && !contentTypePredicate.evaluate(contentType)) {
|
||||
if (contentType != null && !contentTypePredicate.apply(contentType)) {
|
||||
throw new InvalidContentTypeException(contentType, dataSpec);
|
||||
}
|
||||
}
|
||||
@ -496,17 +530,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
|
||||
ByteBuffer readBuffer = this.readBuffer;
|
||||
if (readBuffer == null) {
|
||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
readBuffer.limit(0);
|
||||
this.readBuffer = readBuffer;
|
||||
}
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
while (!readBuffer.hasRemaining()) {
|
||||
// Fill readBuffer with more data from Cronet.
|
||||
operation.close();
|
||||
readBuffer.clear();
|
||||
readInternal(castNonNull(readBuffer));
|
||||
readInternal(readBuffer);
|
||||
|
||||
if (finished) {
|
||||
bytesRemaining = 0;
|
||||
@ -516,14 +545,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
readBuffer.flip();
|
||||
Assertions.checkState(readBuffer.hasRemaining());
|
||||
if (bytesToSkip > 0) {
|
||||
int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip);
|
||||
int bytesSkipped = (int) min(readBuffer.remaining(), bytesToSkip);
|
||||
readBuffer.position(readBuffer.position() + bytesSkipped);
|
||||
bytesToSkip -= bytesSkipped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int bytesRead = Math.min(readBuffer.remaining(), readLength);
|
||||
int bytesRead = min(readBuffer.remaining(), readLength);
|
||||
readBuffer.get(buffer, offset, bytesRead);
|
||||
|
||||
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||
@ -603,11 +632,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
operation.close();
|
||||
|
||||
if (!useCallerBuffer) {
|
||||
if (readBuffer == null) {
|
||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
} else {
|
||||
readBuffer.clear();
|
||||
}
|
||||
ByteBuffer readBuffer = getOrCreateReadBuffer();
|
||||
readBuffer.clear();
|
||||
if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
|
||||
readBuffer.limit((int) bytesToSkip);
|
||||
}
|
||||
@ -781,6 +807,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
private ByteBuffer getOrCreateReadBuffer() {
|
||||
if (readBuffer == null) {
|
||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||
readBuffer.limit(0);
|
||||
}
|
||||
return readBuffer;
|
||||
}
|
||||
|
||||
private static boolean isCompressed(UrlResponseInfo info) {
|
||||
for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
|
||||
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
|
||||
@ -812,7 +846,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
long contentLengthFromRange =
|
||||
Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
|
||||
Long.parseLong(Assertions.checkNotNull(matcher.group(2)))
|
||||
- Long.parseLong(Assertions.checkNotNull(matcher.group(1)))
|
||||
+ 1;
|
||||
if (contentLength < 0) {
|
||||
// Some proxy servers strip the Content-Length header. Fall back to the length
|
||||
// calculated here in this case.
|
||||
@ -824,7 +860,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
// would increase it.
|
||||
Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
|
||||
+ "]");
|
||||
contentLength = Math.max(contentLength, contentLengthFromRange);
|
||||
contentLength = max(contentLength, contentLengthFromRange);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
|
||||
@ -867,7 +903,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
// Copy as much as possible from the src buffer into dst buffer.
|
||||
// Returns the number of bytes copied.
|
||||
private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
|
||||
int remaining = Math.min(src.remaining(), dst.remaining());
|
||||
int remaining = min(src.remaining(), dst.remaining());
|
||||
int limit = src.limit();
|
||||
src.limit(src.position() + remaining);
|
||||
dst.put(src);
|
||||
@ -891,7 +927,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
if (responseCode == 307 || responseCode == 308) {
|
||||
exception =
|
||||
new InvalidResponseCodeException(
|
||||
responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
|
||||
responseCode,
|
||||
info.getHttpStatusText(),
|
||||
info.getAllHeaders(),
|
||||
dataSpec,
|
||||
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
|
||||
operation.open();
|
||||
return;
|
||||
}
|
||||
@ -917,16 +957,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
|
||||
// transformed into a GET.
|
||||
redirectUrlDataSpec =
|
||||
new DataSpec(
|
||||
Uri.parse(newLocationUrl),
|
||||
DataSpec.HTTP_METHOD_GET,
|
||||
/* httpBody= */ null,
|
||||
dataSpec.absoluteStreamPosition,
|
||||
dataSpec.position,
|
||||
dataSpec.length,
|
||||
dataSpec.key,
|
||||
dataSpec.flags,
|
||||
dataSpec.httpRequestHeaders);
|
||||
dataSpec
|
||||
.buildUpon()
|
||||
.setUri(newLocationUrl)
|
||||
.setHttpMethod(DataSpec.HTTP_METHOD_GET)
|
||||
.setHttpBody(null)
|
||||
.build();
|
||||
} else {
|
||||
redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
@ -50,14 +52,13 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
private final HttpDataSource.Factory fallbackFactory;
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
@ -79,23 +80,36 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
*/
|
||||
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor) {
|
||||
this(cronetEngineWrapper, executor, DEFAULT_USER_AGENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||
*/
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
String userAgent) {
|
||||
CronetEngineWrapper cronetEngineWrapper, Executor executor, String userAgent) {
|
||||
this(
|
||||
cronetEngineWrapper,
|
||||
executor,
|
||||
@ -112,7 +126,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
@ -147,7 +161,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
@ -178,14 +192,13 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
@ -209,14 +222,33 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||
* cross-protocol redirects.
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
* @param transferListener An optional listener.
|
||||
*/
|
||||
public CronetDataSourceFactory(
|
||||
CronetEngineWrapper cronetEngineWrapper,
|
||||
Executor executor,
|
||||
@Nullable TransferListener transferListener) {
|
||||
this(cronetEngineWrapper, executor, transferListener, DEFAULT_USER_AGENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
*
|
||||
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
|
||||
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
|
||||
*
|
||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||
@ -244,7 +276,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
|
||||
* DefaultHttpDataSourceFactory} will be used instead.
|
||||
@ -277,7 +309,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a CronetDataSourceFactory.
|
||||
* Creates an instance.
|
||||
*
|
||||
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -166,7 +168,8 @@ public final class CronetEngineWrapper {
|
||||
private final boolean preferGMSCoreCronet;
|
||||
|
||||
// Multi-catch can only be used for API 19+ in this case.
|
||||
@SuppressWarnings("UseMultiCatch")
|
||||
// Field#get(null) is blocked by the null-checker, but is safe because the field is static.
|
||||
@SuppressWarnings({"UseMultiCatch", "nullness:argument.type.incompatible"})
|
||||
public CronetProviderComparator(boolean preferGMSCoreCronet) {
|
||||
// GMSCore CronetProvider classes are only available in some configurations.
|
||||
// Thus, we use reflection to copy static name.
|
||||
@ -229,7 +232,7 @@ public final class CronetEngineWrapper {
|
||||
}
|
||||
String[] versionStringsLeft = Util.split(versionLeft, "\\.");
|
||||
String[] versionStringsRight = Util.split(versionRight, "\\.");
|
||||
int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);
|
||||
int minLength = min(versionStringsLeft.length, versionStringsRight.length);
|
||||
for (int i = 0; i < minLength; i++) {
|
||||
if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
|
||||
try {
|
||||
|
@ -48,18 +48,18 @@ public final class ByteArrayUploadDataProviderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLength() {
|
||||
public void getLength() {
|
||||
assertThat(byteArrayUploadDataProvider.getLength()).isEqualTo(TEST_DATA.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFullBuffer() throws IOException {
|
||||
public void readFullBuffer() throws IOException {
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadPartialBuffer() throws IOException {
|
||||
public void readPartialBuffer() throws IOException {
|
||||
byte[] firstHalf = Arrays.copyOf(TEST_DATA, TEST_DATA.length / 2);
|
||||
byte[] secondHalf = Arrays.copyOfRange(TEST_DATA, TEST_DATA.length / 2, TEST_DATA.length);
|
||||
byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);
|
||||
@ -75,7 +75,7 @@ public final class ByteArrayUploadDataProviderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRewind() throws IOException {
|
||||
public void rewind() throws IOException {
|
||||
// Read all the data.
|
||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||
|
@ -16,10 +16,11 @@
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.lang.Math.min;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@ -64,6 +65,7 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
/** Tests for {@link CronetDataSource}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@ -121,12 +123,15 @@ public final class CronetDataSourceTest {
|
||||
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
|
||||
mockStatusResponse();
|
||||
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL));
|
||||
testPostDataSpec =
|
||||
new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);
|
||||
new DataSpec.Builder()
|
||||
.setUri(TEST_URL)
|
||||
.setHttpMethod(DataSpec.HTTP_METHOD_POST)
|
||||
.setHttpBody(TEST_POST_BODY)
|
||||
.build();
|
||||
testHeadDataSpec =
|
||||
new DataSpec(
|
||||
Uri.parse(TEST_URL), DataSpec.HTTP_METHOD_HEAD, null, 0, 0, C.LENGTH_UNSET, null, 0);
|
||||
new DataSpec.Builder().setUri(TEST_URL).setHttpMethod(DataSpec.HTTP_METHOD_HEAD).build();
|
||||
testResponseHeader = new HashMap<>();
|
||||
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
|
||||
// This value can be anything since the DataSpec is unset.
|
||||
@ -199,7 +204,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpeningTwiceThrows() throws HttpDataSourceException {
|
||||
public void openingTwiceThrows() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
try {
|
||||
@ -211,7 +216,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCallbackFromPreviousRequest() throws HttpDataSourceException {
|
||||
public void callbackFromPreviousRequest() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
@ -234,7 +239,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestStartCalled() throws HttpDataSourceException {
|
||||
public void requestStartCalled() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
@ -244,8 +249,8 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestSetsRangeHeader() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
public void requestSetsRangeHeader() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
@ -254,8 +259,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestHeadersSet() throws HttpDataSourceException {
|
||||
|
||||
public void requestHeadersSet() throws HttpDataSourceException {
|
||||
Map<String, String> headersSet = new HashMap<>();
|
||||
doAnswer(
|
||||
(invocation) -> {
|
||||
@ -275,17 +279,14 @@ public final class CronetDataSourceTest {
|
||||
dataSpecRequestProperties.put("defaultHeader3", "dataSpecOverridesAll");
|
||||
dataSpecRequestProperties.put("dataSourceHeader2", "dataSpecOverridesDataSource");
|
||||
dataSpecRequestProperties.put("dataSpecHeader1", "dataSpecValue1");
|
||||
|
||||
testDataSpec =
|
||||
new DataSpec(
|
||||
/* uri= */ Uri.parse(TEST_URL),
|
||||
/* httpMethod= */ DataSpec.HTTP_METHOD_GET,
|
||||
/* httpBody= */ null,
|
||||
/* absoluteStreamPosition= */ 1000,
|
||||
/* position= */ 1000,
|
||||
/* length= */ 5000,
|
||||
/* key= */ null,
|
||||
/* flags= */ 0,
|
||||
dataSpecRequestProperties);
|
||||
new DataSpec.Builder()
|
||||
.setUri(TEST_URL)
|
||||
.setPosition(1000)
|
||||
.setLength(5000)
|
||||
.setHttpRequestHeaders(dataSpecRequestProperties)
|
||||
.build();
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
@ -301,7 +302,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestOpen() throws HttpDataSourceException {
|
||||
public void requestOpen() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
||||
verify(mockTransferListener)
|
||||
@ -309,9 +310,8 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestOpenGzippedCompressedReturnsDataSpecLength()
|
||||
throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000, null);
|
||||
public void requestOpenGzippedCompressedReturnsDataSpecLength() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000);
|
||||
testResponseHeader.put("Content-Encoding", "gzip");
|
||||
testResponseHeader.put("Content-Length", Long.toString(50L));
|
||||
mockResponseStartSuccess();
|
||||
@ -322,7 +322,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestOpenFail() {
|
||||
public void requestOpenFail() {
|
||||
mockResponseStartFailure();
|
||||
|
||||
try {
|
||||
@ -340,14 +340,14 @@ public final class CronetDataSourceTest {
|
||||
@Test
|
||||
public void open_ifBodyIsSetWithoutContentTypeHeader_fails() {
|
||||
testDataSpec =
|
||||
new DataSpec(
|
||||
/* uri= */ Uri.parse(TEST_URL),
|
||||
/* postBody= */ new byte[1024],
|
||||
/* absoluteStreamPosition= */ 200,
|
||||
/* position= */ 200,
|
||||
/* length= */ 1024,
|
||||
/* key= */ "key",
|
||||
/* flags= */ 0);
|
||||
new DataSpec.Builder()
|
||||
.setUri(TEST_URL)
|
||||
.setHttpMethod(DataSpec.HTTP_METHOD_POST)
|
||||
.setHttpBody(new byte[1024])
|
||||
.setPosition(200)
|
||||
.setLength(1024)
|
||||
.setKey("key")
|
||||
.build();
|
||||
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
@ -358,7 +358,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestOpenFailDueToDnsFailure() {
|
||||
public void requestOpenFailDueToDnsFailure() {
|
||||
mockResponseStartFailure();
|
||||
when(mockNetworkException.getErrorCode())
|
||||
.thenReturn(NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
|
||||
@ -376,15 +376,18 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestOpenValidatesStatusCode() {
|
||||
public void requestOpenPropagatesFailureResponseBody() throws Exception {
|
||||
mockResponseStartSuccess();
|
||||
testUrlResponseInfo = createUrlResponseInfo(500); // statusCode
|
||||
// Use a size larger than CronetDataSource.READ_BUFFER_SIZE_BYTES
|
||||
int responseLength = 40 * 1024;
|
||||
mockReadSuccess(/* position= */ 0, /* length= */ responseLength);
|
||||
testUrlResponseInfo = createUrlResponseInfo(/* statusCode= */ 500);
|
||||
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
fail("HttpDataSource.HttpDataSourceException expected");
|
||||
} catch (HttpDataSourceException e) {
|
||||
assertThat(e).isInstanceOf(HttpDataSource.InvalidResponseCodeException.class);
|
||||
fail("HttpDataSource.InvalidResponseCodeException expected");
|
||||
} catch (HttpDataSource.InvalidResponseCodeException e) {
|
||||
assertThat(e.responseBody).isEqualTo(buildTestDataArray(0, responseLength));
|
||||
// Check for connection not automatically closed.
|
||||
verify(mockUrlRequest, never()).cancel();
|
||||
verify(mockTransferListener, never())
|
||||
@ -393,7 +396,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestOpenValidatesContentTypePredicate() {
|
||||
public void requestOpenValidatesContentTypePredicate() {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
ArrayList<String> testedContentTypes = new ArrayList<>();
|
||||
@ -416,7 +419,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostRequestOpen() throws HttpDataSourceException {
|
||||
public void postRequestOpen() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||
@ -426,7 +429,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostRequestOpenValidatesContentType() {
|
||||
public void postRequestOpenValidatesContentType() {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
try {
|
||||
@ -438,7 +441,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPostRequestOpenRejects307Redirects() {
|
||||
public void postRequestOpenRejects307Redirects() {
|
||||
mockResponseStartSuccess();
|
||||
mockResponseStartRedirect();
|
||||
|
||||
@ -452,7 +455,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeadRequestOpen() throws HttpDataSourceException {
|
||||
public void headRequestOpen() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
dataSourceUnderTest.open(testHeadDataSpec);
|
||||
verify(mockTransferListener)
|
||||
@ -461,7 +464,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestReadTwice() throws HttpDataSourceException {
|
||||
public void requestReadTwice() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
@ -484,7 +487,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecondRequestNoContentLength() throws HttpDataSourceException {
|
||||
public void secondRequestNoContentLength() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
testResponseHeader.put("Content-Length", Long.toString(1L));
|
||||
mockReadSuccess(0, 16);
|
||||
@ -510,7 +513,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadWithOffset() throws HttpDataSourceException {
|
||||
public void readWithOffset() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
@ -525,11 +528,11 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRangeRequestWith206Response() throws HttpDataSourceException {
|
||||
public void rangeRequestWith206Response() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(1000, 5000);
|
||||
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
@ -542,11 +545,11 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRangeRequestWith200Response() throws HttpDataSourceException {
|
||||
public void rangeRequestWith200Response() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 7000);
|
||||
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
@ -559,7 +562,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadWithUnsetLength() throws HttpDataSourceException {
|
||||
public void readWithUnsetLength() throws HttpDataSourceException {
|
||||
testResponseHeader.remove("Content-Length");
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
@ -575,7 +578,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadReturnsWhatItCan() throws HttpDataSourceException {
|
||||
public void readReturnsWhatItCan() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
@ -590,7 +593,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClosedMeansClosed() throws HttpDataSourceException {
|
||||
public void closedMeansClosed() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
@ -618,8 +621,8 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverread() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
|
||||
public void overread() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16);
|
||||
testResponseHeader.put("Content-Length", Long.toString(16L));
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
@ -671,7 +674,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestReadByteBufferTwice() throws HttpDataSourceException {
|
||||
public void requestReadByteBufferTwice() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
@ -697,7 +700,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestIntermixRead() throws HttpDataSourceException {
|
||||
public void requestIntermixRead() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
// Chunking reads into parts 6, 7, 8, 9.
|
||||
mockReadSuccess(0, 30);
|
||||
@ -739,7 +742,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
|
||||
public void secondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
testResponseHeader.put("Content-Length", Long.toString(1L));
|
||||
mockReadSuccess(0, 16);
|
||||
@ -768,11 +771,11 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||
public void rangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(1000, 5000);
|
||||
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
@ -786,12 +789,12 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||
public void rangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||
// Tests for skipping bytes.
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 7000);
|
||||
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
@ -805,7 +808,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByteBufferWithUnsetLength() throws HttpDataSourceException {
|
||||
public void readByteBufferWithUnsetLength() throws HttpDataSourceException {
|
||||
testResponseHeader.remove("Content-Length");
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
@ -823,7 +826,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByteBufferReturnsWhatItCan() throws HttpDataSourceException {
|
||||
public void readByteBufferReturnsWhatItCan() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
@ -839,8 +842,8 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOverreadByteBuffer() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
|
||||
public void overreadByteBuffer() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16);
|
||||
testResponseHeader.put("Content-Length", Long.toString(16L));
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
@ -895,7 +898,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClosedMeansClosedReadByteBuffer() throws HttpDataSourceException {
|
||||
public void closedMeansClosedReadByteBuffer() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadSuccess(0, 16);
|
||||
|
||||
@ -925,7 +928,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectTimeout() throws InterruptedException {
|
||||
public void connectTimeout() throws InterruptedException {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||
@ -951,10 +954,12 @@ public final class CronetDataSourceTest {
|
||||
// We should still be trying to open.
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// We should still be trying to open as we approach the timeout.
|
||||
SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
setSystemClockInMsAndTriggerPendingMessages(
|
||||
/* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// Now we timeout.
|
||||
SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
|
||||
setSystemClockInMsAndTriggerPendingMessages(
|
||||
/* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
|
||||
timedOutLatch.await();
|
||||
|
||||
verify(mockTransferListener, never())
|
||||
@ -962,7 +967,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectInterrupted() throws InterruptedException {
|
||||
public void connectInterrupted() throws InterruptedException {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||
@ -990,7 +995,8 @@ public final class CronetDataSourceTest {
|
||||
// We should still be trying to open.
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// We should still be trying to open as we approach the timeout.
|
||||
SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
setSystemClockInMsAndTriggerPendingMessages(
|
||||
/* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// Now we interrupt.
|
||||
thread.interrupt();
|
||||
@ -1001,7 +1007,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectResponseBeforeTimeout() throws Exception {
|
||||
public void connectResponseBeforeTimeout() throws Exception {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final CountDownLatch openLatch = new CountDownLatch(1);
|
||||
@ -1024,7 +1030,8 @@ public final class CronetDataSourceTest {
|
||||
// We should still be trying to open.
|
||||
assertNotCountedDown(openLatch);
|
||||
// We should still be trying to open as we approach the timeout.
|
||||
SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
setSystemClockInMsAndTriggerPendingMessages(
|
||||
/* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(openLatch);
|
||||
// The response arrives just in time.
|
||||
dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||
@ -1033,7 +1040,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectIncreasesConnectionTimeout() throws Exception {
|
||||
public void redirectIncreasesConnectionTimeout() throws Exception {
|
||||
long startTimeMs = SystemClock.elapsedRealtime();
|
||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||
@ -1059,14 +1066,15 @@ public final class CronetDataSourceTest {
|
||||
// We should still be trying to open.
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// We should still be trying to open as we approach the timeout.
|
||||
SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
setSystemClockInMsAndTriggerPendingMessages(
|
||||
/* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// A redirect arrives just in time.
|
||||
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
|
||||
|
||||
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
|
||||
SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1);
|
||||
setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs - 1);
|
||||
// We should still be trying to open as we approach the new timeout.
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// A redirect arrives just in time.
|
||||
@ -1074,11 +1082,11 @@ public final class CronetDataSourceTest {
|
||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
|
||||
|
||||
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
|
||||
SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1);
|
||||
setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs - 1);
|
||||
// We should still be trying to open as we approach the new timeout.
|
||||
assertNotCountedDown(timedOutLatch);
|
||||
// Now we timeout.
|
||||
SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10);
|
||||
setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs + 10);
|
||||
timedOutLatch.await();
|
||||
|
||||
verify(mockTransferListener, never())
|
||||
@ -1087,7 +1095,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect()
|
||||
public void redirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect()
|
||||
throws HttpDataSourceException {
|
||||
mockSingleRedirectSuccess();
|
||||
mockFollowRedirectSuccess();
|
||||
@ -1132,7 +1140,7 @@ public final class CronetDataSourceTest {
|
||||
public void
|
||||
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
|
||||
throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||
dataSourceUnderTest =
|
||||
new CronetDataSource(
|
||||
mockCronetEngine,
|
||||
@ -1159,7 +1167,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectNoSetCookieFollowsRedirect() throws HttpDataSourceException {
|
||||
public void redirectNoSetCookieFollowsRedirect() throws HttpDataSourceException {
|
||||
mockSingleRedirectSuccess();
|
||||
mockFollowRedirectSuccess();
|
||||
|
||||
@ -1169,7 +1177,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
||||
public void redirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
||||
throws HttpDataSourceException {
|
||||
dataSourceUnderTest =
|
||||
new CronetDataSource(
|
||||
@ -1191,7 +1199,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExceptionFromTransferListener() throws HttpDataSourceException {
|
||||
public void exceptionFromTransferListener() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
|
||||
// Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that
|
||||
@ -1211,7 +1219,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadFailure() throws HttpDataSourceException {
|
||||
public void readFailure() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadFailure();
|
||||
|
||||
@ -1226,7 +1234,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByteBufferFailure() throws HttpDataSourceException {
|
||||
public void readByteBufferFailure() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadFailure();
|
||||
|
||||
@ -1241,7 +1249,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadNonDirectedByteBufferFailure() throws HttpDataSourceException {
|
||||
public void readNonDirectedByteBufferFailure() throws HttpDataSourceException {
|
||||
mockResponseStartSuccess();
|
||||
mockReadFailure();
|
||||
|
||||
@ -1256,7 +1264,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||
public void readInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||
mockResponseStartSuccess();
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
@ -1287,7 +1295,7 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||
public void readByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||
mockResponseStartSuccess();
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
||||
@ -1318,8 +1326,8 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllowDirectExecutor() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||
public void allowDirectExecutor() throws HttpDataSourceException {
|
||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||
mockResponseStartSuccess();
|
||||
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
@ -1416,7 +1424,7 @@ public final class CronetDataSourceTest {
|
||||
mockUrlRequest, testUrlResponseInfo);
|
||||
} else {
|
||||
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
|
||||
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining());
|
||||
int readLength = min(positionAndRemaining[1], inputBuffer.remaining());
|
||||
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
|
||||
positionAndRemaining[0] += readLength;
|
||||
positionAndRemaining[1] -= readLength;
|
||||
@ -1508,4 +1516,9 @@ public final class CronetDataSourceTest {
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static void setSystemClockInMsAndTriggerPendingMessages(long nowMs) {
|
||||
SystemClock.setCurrentTimeMillis(nowMs);
|
||||
ShadowLooper.idleMainLooper();
|
||||
}
|
||||
}
|
||||
|
@ -18,14 +18,14 @@ its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||
[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:
|
||||
In addition, it's necessary to manually build the FFmpeg library, so that gradle
|
||||
can bundle the FFmpeg binaries in the APK:
|
||||
|
||||
* Set the following shell variable:
|
||||
|
||||
```
|
||||
cd "<path to exoplayer checkout>"
|
||||
FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni"
|
||||
FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main"
|
||||
```
|
||||
|
||||
* Download the [Android NDK][] and set its location in a shell variable.
|
||||
@ -41,6 +41,21 @@ NDK_PATH="<path to Android NDK>"
|
||||
HOST_PLATFORM="linux-x86_64"
|
||||
```
|
||||
|
||||
* Fetch FFmpeg:
|
||||
|
||||
```
|
||||
cd "${FFMPEG_EXT_PATH}/jni" && \
|
||||
git clone git://source.ffmpeg.org/ffmpeg ffmpeg
|
||||
```
|
||||
|
||||
* Checkout an appropriate branch of FFmpeg. We cannot guarantee compatibility
|
||||
with all versions of FFmpeg. We currently recommend version 4.2:
|
||||
|
||||
```
|
||||
cd "${FFMPEG_EXT_PATH}/jni/ffmpeg" && \
|
||||
git checkout release/4.2
|
||||
```
|
||||
|
||||
* Configure the decoders to include. See the [Supported formats][] page for
|
||||
details of the available decoders, and which formats they support.
|
||||
|
||||
@ -48,24 +63,16 @@ HOST_PLATFORM="linux-x86_64"
|
||||
ENABLED_DECODERS=(vorbis opus flac)
|
||||
```
|
||||
|
||||
* Fetch and build FFmpeg. Executing `build_ffmpeg.sh` will fetch and build
|
||||
FFmpeg 4.2 for `armeabi-v7a`, `arm64-v8a`, `x86` and `x86_64`. The script can
|
||||
be edited if you need to build for different architectures.
|
||||
* Execute `build_ffmpeg.sh` to build FFmpeg for `armeabi-v7a`, `arm64-v8a`,
|
||||
`x86` and `x86_64`. The script can be edited if you need to build for
|
||||
different architectures:
|
||||
|
||||
```
|
||||
cd "${FFMPEG_EXT_PATH}" && \
|
||||
cd "${FFMPEG_EXT_PATH}/jni" && \
|
||||
./build_ffmpeg.sh \
|
||||
"${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
|
||||
```
|
||||
|
||||
* Build the JNI native libraries, setting `APP_ABI` to include the architectures
|
||||
built in the previous step. For example:
|
||||
|
||||
```
|
||||
cd "${FFMPEG_EXT_PATH}" && \
|
||||
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86 x86_64" -j4
|
||||
```
|
||||
|
||||
## Build instructions (Windows) ##
|
||||
|
||||
We do not provide support for building this extension on Windows, however it
|
||||
|
@ -11,35 +11,27 @@
|
||||
// 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'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
// The directory from which to pick the ffmpeg binaries.
|
||||
jniLibs.srcDir 'src/main/libs'
|
||||
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
||||
}
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
// Configure the native build only if ffmpeg is present to avoid gradle sync
|
||||
// failures if ffmpeg hasn't been built according to the README instructions.
|
||||
if (project.file('src/main/jni/ffmpeg').exists()) {
|
||||
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
|
||||
android.externalNativeBuild.cmake.version = '3.7.1+'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
@ -28,19 +28,18 @@ import com.google.android.exoplayer2.util.Util;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* FFmpeg audio decoder.
|
||||
*/
|
||||
/* package */ final class FfmpegDecoder extends
|
||||
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
|
||||
/** FFmpeg audio decoder. */
|
||||
/* package */ final class FfmpegAudioDecoder
|
||||
extends SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
|
||||
|
||||
// Output buffer sizes when decoding PCM mu-law streams, which is the maximum FFmpeg outputs.
|
||||
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
|
||||
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
||||
|
||||
// Error codes matching ffmpeg_jni.cc.
|
||||
private static final int DECODER_ERROR_INVALID_DATA = -1;
|
||||
private static final int DECODER_ERROR_OTHER = -2;
|
||||
// LINT.IfChange
|
||||
private static final int AUDIO_DECODER_ERROR_INVALID_DATA = -1;
|
||||
private static final int AUDIO_DECODER_ERROR_OTHER = -2;
|
||||
// LINT.ThenChange(../../../../../../../jni/ffmpeg_jni.cc)
|
||||
|
||||
private final String codecName;
|
||||
@Nullable private final byte[] extraData;
|
||||
@ -52,11 +51,11 @@ import java.util.List;
|
||||
private volatile int channelCount;
|
||||
private volatile int sampleRate;
|
||||
|
||||
public FfmpegDecoder(
|
||||
public FfmpegAudioDecoder(
|
||||
Format format,
|
||||
int numInputBuffers,
|
||||
int numOutputBuffers,
|
||||
int initialInputBufferSize,
|
||||
Format format,
|
||||
boolean outputFloat)
|
||||
throws FfmpegDecoderException {
|
||||
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
|
||||
@ -83,12 +82,14 @@ import java.util.List;
|
||||
|
||||
@Override
|
||||
protected DecoderInputBuffer createInputBuffer() {
|
||||
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
|
||||
return new DecoderInputBuffer(
|
||||
DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT,
|
||||
FfmpegLibrary.getInputBufferPaddingSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SimpleOutputBuffer createOutputBuffer() {
|
||||
return new SimpleOutputBuffer(this);
|
||||
return new SimpleOutputBuffer(this::releaseOutputBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -109,13 +110,13 @@ import java.util.List;
|
||||
int inputSize = inputData.limit();
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
|
||||
if (result == DECODER_ERROR_INVALID_DATA) {
|
||||
if (result == AUDIO_DECODER_ERROR_INVALID_DATA) {
|
||||
// Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will
|
||||
// be produced for this buffer, so mark it as decode-only to ensure that the audio sink's
|
||||
// position is reset when more audio is produced.
|
||||
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
return null;
|
||||
} else if (result == DECODER_ERROR_OTHER) {
|
||||
} else if (result == AUDIO_DECODER_ERROR_OTHER) {
|
||||
return new FfmpegDecoderException("Error decoding (see logcat).");
|
||||
}
|
||||
if (!hasOutputFormat) {
|
||||
@ -123,8 +124,8 @@ import java.util.List;
|
||||
sampleRate = ffmpegGetSampleRate(nativeContext);
|
||||
if (sampleRate == 0 && "alac".equals(codecName)) {
|
||||
Assertions.checkNotNull(extraData);
|
||||
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
|
||||
// See https://trac.ffmpeg.org/ticket/6096
|
||||
// ALAC decoder did not set the sample rate in earlier versions of FFmpeg. See
|
||||
// https://trac.ffmpeg.org/ticket/6096.
|
||||
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
|
||||
parsableExtraData.setPosition(extraData.length - 4);
|
||||
sampleRate = parsableExtraData.readUnsignedIntToInt();
|
||||
@ -153,9 +154,7 @@ import java.util.List;
|
||||
return sampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encoding of output audio.
|
||||
*/
|
||||
/** Returns the encoding of output audio. */
|
||||
public @C.Encoding int getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
@ -217,13 +216,14 @@ import java.util.List;
|
||||
int rawSampleRate,
|
||||
int rawChannelCount);
|
||||
|
||||
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
|
||||
ByteBuffer outputData, int outputSize);
|
||||
private native int ffmpegDecode(
|
||||
long context, ByteBuffer inputData, int inputSize, ByteBuffer outputData, int outputSize);
|
||||
|
||||
private native int ffmpegGetChannelCount(long context);
|
||||
|
||||
private native int ffmpegGetSampleRate(long context);
|
||||
|
||||
private native long ffmpegReset(long context, @Nullable byte[] extraData);
|
||||
|
||||
private native void ffmpegRelease(long context);
|
||||
|
||||
}
|
@ -15,42 +15,43 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY;
|
||||
import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_SUPPORTED_WITH_TRANSCODING;
|
||||
import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_UNSUPPORTED;
|
||||
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.AudioSink.SinkFormatSupport;
|
||||
import com.google.android.exoplayer2.audio.DecoderAudioRenderer;
|
||||
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;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.Collections;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* Decodes and renders audio using FFmpeg.
|
||||
*/
|
||||
public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
/** Decodes and renders audio using FFmpeg. */
|
||||
public final class FfmpegAudioRenderer extends DecoderAudioRenderer<FfmpegAudioDecoder> {
|
||||
|
||||
private static final String TAG = "FfmpegAudioRenderer";
|
||||
|
||||
/** The number of input and output buffers. */
|
||||
private static final int NUM_BUFFERS = 16;
|
||||
/** The default input buffer size. */
|
||||
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
|
||||
|
||||
private final boolean enableFloatOutput;
|
||||
|
||||
private @MonotonicNonNull FfmpegDecoder decoder;
|
||||
|
||||
public FfmpegAudioRenderer() {
|
||||
this(/* eventHandler= */ null, /* eventListener= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @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.
|
||||
@ -63,44 +64,43 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
this(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors),
|
||||
/* enableFloatOutput= */ false);
|
||||
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @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(
|
||||
@Nullable Handler eventHandler,
|
||||
@Nullable AudioRendererEventListener eventListener,
|
||||
AudioSink audioSink,
|
||||
boolean enableFloatOutput) {
|
||||
AudioSink audioSink) {
|
||||
super(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
/* drmSessionManager= */ null,
|
||||
/* playClearSamplesWithoutKeys= */ false,
|
||||
audioSink);
|
||||
this.enableFloatOutput = enableFloatOutput;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
@FormatSupport
|
||||
protected int supportsFormatInternal(
|
||||
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
|
||||
Assertions.checkNotNull(format.sampleMimeType);
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
String mimeType = Assertions.checkNotNull(format.sampleMimeType);
|
||||
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType) || !isOutputSupported(format)) {
|
||||
} else if (!FfmpegLibrary.supportsFormat(mimeType)
|
||||
|| (!sinkSupportsFormat(format, C.ENCODING_PCM_16BIT)
|
||||
&& !sinkSupportsFormat(format, C.ENCODING_PCM_FLOAT))) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
} else if (format.exoMediaCryptoType != null) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
return FORMAT_HANDLED;
|
||||
@ -109,65 +109,64 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
|
||||
@Override
|
||||
@AdaptiveSupport
|
||||
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
|
||||
public final int supportsMixedMimeTypeAdaptation() {
|
||||
return ADAPTIVE_NOT_SEAMLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||
protected FfmpegAudioDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||
throws FfmpegDecoderException {
|
||||
TraceUtil.beginSection("createFfmpegAudioDecoder");
|
||||
int initialInputBufferSize =
|
||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||
decoder =
|
||||
new FfmpegDecoder(
|
||||
NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));
|
||||
FfmpegAudioDecoder decoder =
|
||||
new FfmpegAudioDecoder(
|
||||
format, NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, shouldOutputFloat(format));
|
||||
TraceUtil.endSection();
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Format getOutputFormat() {
|
||||
public Format getOutputFormat(FfmpegAudioDecoder decoder) {
|
||||
Assertions.checkNotNull(decoder);
|
||||
int channelCount = decoder.getChannelCount();
|
||||
int sampleRate = decoder.getSampleRate();
|
||||
@C.PcmEncoding int encoding = decoder.getEncoding();
|
||||
return Format.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
/* codecs= */ null,
|
||||
Format.NO_VALUE,
|
||||
Format.NO_VALUE,
|
||||
channelCount,
|
||||
sampleRate,
|
||||
encoding,
|
||||
Collections.emptyList(),
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null);
|
||||
return new Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||
.setChannelCount(decoder.getChannelCount())
|
||||
.setSampleRate(decoder.getSampleRate())
|
||||
.setPcmEncoding(decoder.getEncoding())
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean isOutputSupported(Format inputFormat) {
|
||||
return shouldUseFloatOutput(inputFormat)
|
||||
|| supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_16BIT);
|
||||
/**
|
||||
* Returns whether the renderer's {@link AudioSink} supports the PCM format that will be output
|
||||
* from the decoder for the given input format and requested output encoding.
|
||||
*/
|
||||
private boolean sinkSupportsFormat(Format inputFormat, @C.PcmEncoding int pcmEncoding) {
|
||||
return sinkSupportsFormat(
|
||||
Util.getPcmFormat(pcmEncoding, inputFormat.channelCount, inputFormat.sampleRate));
|
||||
}
|
||||
|
||||
private boolean shouldUseFloatOutput(Format inputFormat) {
|
||||
Assertions.checkNotNull(inputFormat.sampleMimeType);
|
||||
if (!enableFloatOutput || !supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_FLOAT)) {
|
||||
return false;
|
||||
private boolean shouldOutputFloat(Format inputFormat) {
|
||||
if (!sinkSupportsFormat(inputFormat, C.ENCODING_PCM_16BIT)) {
|
||||
// We have no choice because the sink doesn't support 16-bit integer PCM.
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
|
||||
@SinkFormatSupport
|
||||
int formatSupport =
|
||||
getSinkFormatSupport(
|
||||
Util.getPcmFormat(
|
||||
C.ENCODING_PCM_FLOAT, inputFormat.channelCount, inputFormat.sampleRate));
|
||||
switch (formatSupport) {
|
||||
case SINK_FORMAT_SUPPORTED_DIRECTLY:
|
||||
// AC-3 is always 16-bit, so there's no point using floating point. Assume that it's worth
|
||||
// using for all other formats.
|
||||
return !MimeTypes.AUDIO_AC3.equals(inputFormat.sampleMimeType);
|
||||
case SINK_FORMAT_UNSUPPORTED:
|
||||
case SINK_FORMAT_SUPPORTED_WITH_TRANSCODING:
|
||||
default:
|
||||
// For all other formats, assume that it's worth using 32-bit float encoding.
|
||||
return true;
|
||||
// Always prefer 16-bit PCM if the sink does not provide direct support for floating point.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,12 +15,10 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import com.google.android.exoplayer2.audio.AudioDecoderException;
|
||||
import com.google.android.exoplayer2.decoder.DecoderException;
|
||||
|
||||
/**
|
||||
* Thrown when an FFmpeg decoder error occurs.
|
||||
*/
|
||||
public final class FfmpegDecoderException extends AudioDecoderException {
|
||||
/** Thrown when an FFmpeg decoder error occurs. */
|
||||
public final class FfmpegDecoderException extends DecoderException {
|
||||
|
||||
/* package */ FfmpegDecoderException(String message) {
|
||||
super(message);
|
||||
|
@ -16,10 +16,12 @@
|
||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* Configures and queries the underlying native library.
|
||||
@ -33,7 +35,10 @@ public final class FfmpegLibrary {
|
||||
private static final String TAG = "FfmpegLibrary";
|
||||
|
||||
private static final LibraryLoader LOADER =
|
||||
new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg");
|
||||
new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg_jni");
|
||||
|
||||
private static @MonotonicNonNull String version;
|
||||
private static int inputBufferPaddingSize = C.LENGTH_UNSET;
|
||||
|
||||
private FfmpegLibrary() {}
|
||||
|
||||
@ -56,8 +61,29 @@ public final class FfmpegLibrary {
|
||||
}
|
||||
|
||||
/** Returns the version of the underlying library if available, or null otherwise. */
|
||||
public static @Nullable String getVersion() {
|
||||
return isAvailable() ? ffmpegGetVersion() : null;
|
||||
@Nullable
|
||||
public static String getVersion() {
|
||||
if (!isAvailable()) {
|
||||
return null;
|
||||
}
|
||||
if (version == null) {
|
||||
version = ffmpegGetVersion();
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the required amount of padding for input buffers in bytes, or {@link C#LENGTH_UNSET} if
|
||||
* the underlying library is not available.
|
||||
*/
|
||||
public static int getInputBufferPaddingSize() {
|
||||
if (!isAvailable()) {
|
||||
return C.LENGTH_UNSET;
|
||||
}
|
||||
if (inputBufferPaddingSize == C.LENGTH_UNSET) {
|
||||
inputBufferPaddingSize = ffmpegGetInputBufferPaddingSize();
|
||||
}
|
||||
return inputBufferPaddingSize;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,7 +95,7 @@ public final class FfmpegLibrary {
|
||||
if (!isAvailable()) {
|
||||
return false;
|
||||
}
|
||||
String codecName = getCodecName(mimeType);
|
||||
@Nullable String codecName = getCodecName(mimeType);
|
||||
if (codecName == null) {
|
||||
return false;
|
||||
}
|
||||
@ -84,7 +110,8 @@ public final class FfmpegLibrary {
|
||||
* Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
|
||||
* if it's unsupported.
|
||||
*/
|
||||
/* package */ static @Nullable String getCodecName(String mimeType) {
|
||||
@Nullable
|
||||
/* package */ static String getCodecName(String mimeType) {
|
||||
switch (mimeType) {
|
||||
case MimeTypes.AUDIO_AAC:
|
||||
return "aac";
|
||||
@ -118,12 +145,18 @@ public final class FfmpegLibrary {
|
||||
return "pcm_mulaw";
|
||||
case MimeTypes.AUDIO_ALAW:
|
||||
return "pcm_alaw";
|
||||
case MimeTypes.VIDEO_H264:
|
||||
return "h264";
|
||||
case MimeTypes.VIDEO_H265:
|
||||
return "hevc";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static native String ffmpegGetVersion();
|
||||
private static native boolean ffmpegHasDecoder(String codecName);
|
||||
|
||||
private static native int ffmpegGetInputBufferPaddingSize();
|
||||
|
||||
private static native boolean ffmpegHasDecoder(String codecName);
|
||||
}
|
||||
|
@ -1,40 +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.
|
||||
#
|
||||
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavcodec
|
||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libswresample
|
||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libavutil
|
||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := ffmpeg
|
||||
LOCAL_SRC_FILES := ffmpeg_jni.cc
|
||||
LOCAL_C_INCLUDES := ffmpeg
|
||||
LOCAL_SHARED_LIBRARIES := libavcodec libswresample libavutil
|
||||
LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog
|
||||
include $(BUILD_SHARED_LIBRARY)
|
@ -1,20 +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.
|
||||
#
|
||||
|
||||
APP_OPTIM := release
|
||||
APP_STL := c++_static
|
||||
APP_CPPFLAGS := -frtti
|
||||
APP_PLATFORM := android-9
|
38
extensions/ffmpeg/src/main/jni/CMakeLists.txt
Normal file
38
extensions/ffmpeg/src/main/jni/CMakeLists.txt
Normal file
@ -0,0 +1,38 @@
|
||||
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
|
||||
|
||||
# Enable C++11 features.
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
project(libffmpeg_jni C CXX)
|
||||
|
||||
set(ffmpeg_location "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg")
|
||||
set(ffmpeg_binaries "${ffmpeg_location}/android-libs/${ANDROID_ABI}")
|
||||
set(ffmpeg_output_dir "${CMAKE_CURRENT_SOURCE_DIR}/../libs/${ANDROID_ABI}")
|
||||
|
||||
foreach(ffmpeg_lib avutil swresample avcodec)
|
||||
set(ffmpeg_lib_filename lib${ffmpeg_lib}.so)
|
||||
set(ffmpeg_lib_file_path ${ffmpeg_binaries}/${ffmpeg_lib_filename})
|
||||
add_library(
|
||||
${ffmpeg_lib}
|
||||
SHARED
|
||||
IMPORTED)
|
||||
set_target_properties(
|
||||
${ffmpeg_lib} PROPERTIES
|
||||
IMPORTED_LOCATION
|
||||
${ffmpeg_lib_file_path})
|
||||
file(COPY ${ffmpeg_lib_file_path} DESTINATION ${ffmpeg_output_dir})
|
||||
endforeach()
|
||||
|
||||
include_directories(${ffmpeg_location})
|
||||
find_library(android_log_lib log)
|
||||
|
||||
add_library(ffmpeg_jni
|
||||
SHARED
|
||||
ffmpeg_jni.cc)
|
||||
|
||||
target_link_libraries(ffmpeg_jni
|
||||
PRIVATE android
|
||||
PRIVATE avutil
|
||||
PRIVATE swresample
|
||||
PRIVATE avcodec
|
||||
PRIVATE ${android_log_lib})
|
@ -41,10 +41,7 @@ for decoder in "${ENABLED_DECODERS[@]}"
|
||||
do
|
||||
COMMON_OPTIONS="${COMMON_OPTIONS} --enable-decoder=${decoder}"
|
||||
done
|
||||
cd "${FFMPEG_EXT_PATH}"
|
||||
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg)
|
||||
cd ffmpeg
|
||||
git checkout release/4.2
|
||||
cd "${FFMPEG_EXT_PATH}/jni/ffmpeg"
|
||||
./configure \
|
||||
--libdir=android-libs/armeabi-v7a \
|
||||
--arch=arm \
|
||||
|
@ -36,25 +36,25 @@ extern "C" {
|
||||
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \
|
||||
__VA_ARGS__))
|
||||
|
||||
#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \
|
||||
extern "C" { \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegDecoder_ ## NAME \
|
||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__);\
|
||||
} \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegDecoder_ ## NAME \
|
||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
|
||||
#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \
|
||||
extern "C" { \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_##NAME( \
|
||||
JNIEnv *env, jobject thiz, ##__VA_ARGS__); \
|
||||
} \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_##NAME( \
|
||||
JNIEnv *env, jobject thiz, ##__VA_ARGS__)
|
||||
|
||||
#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \
|
||||
extern "C" { \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_ ## NAME \
|
||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__);\
|
||||
} \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_ ## NAME \
|
||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
|
||||
#define AUDIO_DECODER_FUNC(RETURN_TYPE, NAME, ...) \
|
||||
extern "C" { \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegAudioDecoder_##NAME( \
|
||||
JNIEnv *env, jobject thiz, ##__VA_ARGS__); \
|
||||
} \
|
||||
JNIEXPORT RETURN_TYPE \
|
||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegAudioDecoder_##NAME( \
|
||||
JNIEnv *env, jobject thiz, ##__VA_ARGS__)
|
||||
|
||||
#define ERROR_STRING_BUFFER_LENGTH 256
|
||||
|
||||
@ -63,9 +63,10 @@ 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;
|
||||
|
||||
// Error codes matching FfmpegDecoder.java.
|
||||
static const int DECODER_ERROR_INVALID_DATA = -1;
|
||||
static const int DECODER_ERROR_OTHER = -2;
|
||||
// LINT.IfChange
|
||||
static const int AUDIO_DECODER_ERROR_INVALID_DATA = -1;
|
||||
static const int AUDIO_DECODER_ERROR_OTHER = -2;
|
||||
// LINT.ThenChange(../java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java)
|
||||
|
||||
/**
|
||||
* Returns the AVCodec with the specified name, or NULL if it is not available.
|
||||
@ -83,7 +84,8 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
||||
|
||||
/**
|
||||
* Decodes the packet into the output buffer, returning the number of bytes
|
||||
* written, or a negative DECODER_ERROR constant value in the case of an error.
|
||||
* written, or a negative AUDIO_DECODER_ERROR constant value in the case of an
|
||||
* error.
|
||||
*/
|
||||
int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||
uint8_t *outputBuffer, int outputSize);
|
||||
@ -111,12 +113,17 @@ LIBRARY_FUNC(jstring, ffmpegGetVersion) {
|
||||
return env->NewStringUTF(LIBAVCODEC_IDENT);
|
||||
}
|
||||
|
||||
LIBRARY_FUNC(jint, ffmpegGetInputBufferPaddingSize) {
|
||||
return (jint)AV_INPUT_BUFFER_PADDING_SIZE;
|
||||
}
|
||||
|
||||
LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
|
||||
return getCodecByName(env, codecName) != NULL;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
|
||||
jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) {
|
||||
AUDIO_DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName,
|
||||
jbyteArray extraData, jboolean outputFloat,
|
||||
jint rawSampleRate, jint rawChannelCount) {
|
||||
AVCodec *codec = getCodecByName(env, codecName);
|
||||
if (!codec) {
|
||||
LOGE("Codec not found.");
|
||||
@ -126,8 +133,8 @@ DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
|
||||
rawChannelCount);
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
||||
jint inputSize, jobject outputData, jint outputSize) {
|
||||
AUDIO_DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
||||
jint inputSize, jobject outputData, jint outputSize) {
|
||||
if (!context) {
|
||||
LOGE("Context must be non-NULL.");
|
||||
return -1;
|
||||
@ -154,7 +161,7 @@ DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
||||
outputSize);
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) {
|
||||
AUDIO_DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) {
|
||||
if (!context) {
|
||||
LOGE("Context must be non-NULL.");
|
||||
return -1;
|
||||
@ -162,7 +169,7 @@ DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) {
|
||||
return ((AVCodecContext *) context)->channels;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) {
|
||||
AUDIO_DECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) {
|
||||
if (!context) {
|
||||
LOGE("Context must be non-NULL.");
|
||||
return -1;
|
||||
@ -170,7 +177,7 @@ DECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) {
|
||||
return ((AVCodecContext *) context)->sample_rate;
|
||||
}
|
||||
|
||||
DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
|
||||
AUDIO_DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
|
||||
AVCodecContext *context = (AVCodecContext *) jContext;
|
||||
if (!context) {
|
||||
LOGE("Tried to reset without a context.");
|
||||
@ -198,7 +205,7 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
|
||||
return (jlong) context;
|
||||
}
|
||||
|
||||
DECODER_FUNC(void, ffmpegRelease, jlong context) {
|
||||
AUDIO_DECODER_FUNC(void, ffmpegRelease, jlong context) {
|
||||
if (context) {
|
||||
releaseContext((AVCodecContext *) context);
|
||||
}
|
||||
@ -259,8 +266,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||
result = avcodec_send_packet(context, packet);
|
||||
if (result) {
|
||||
logError("avcodec_send_packet", result);
|
||||
return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA
|
||||
: DECODER_ERROR_OTHER;
|
||||
return result == AVERROR_INVALIDDATA ? AUDIO_DECODER_ERROR_INVALID_DATA
|
||||
: AUDIO_DECODER_ERROR_OTHER;
|
||||
}
|
||||
|
||||
// Dequeue output data until it runs out.
|
||||
|
@ -21,12 +21,14 @@ import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */
|
||||
/**
|
||||
* Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class DefaultRenderersFactoryTest {
|
||||
|
||||
@Test
|
||||
public void createRenderers_instantiatesVpxRenderer() {
|
||||
public void createRenderers_instantiatesFfmpegAudioRenderer() {
|
||||
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
|
||||
FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
@ -11,41 +11,29 @@
|
||||
// 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'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDir 'src/main/libs'
|
||||
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
||||
}
|
||||
androidTest.assets.srcDir '../../testdata/src/test/assets/'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
jniLibs.srcDir 'src/main/libs'
|
||||
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
androidTestImplementation project(modulePrefix + 'testutils')
|
||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||
testImplementation 'androidx.test:core:' + androidxTestCoreVersion
|
||||
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation project(modulePrefix + 'testdata')
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
|
||||
*;
|
||||
}
|
||||
-keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
|
||||
-keep class com.google.android.exoplayer2.extractor.FlacStreamMetadata {
|
||||
*;
|
||||
}
|
||||
-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
|
||||
|
@ -23,9 +23,7 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
<uses-library android:name="android.test.runner"/>
|
||||
</application>
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"/>
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="com.google.android.exoplayer2.ext.flac.test"
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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.ext.flac;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.ext.flac.FlacBinarySearchSeeker.OutputFrameHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link FlacBinarySearchSeeker}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class FlacBinarySearchSeekerTest {
|
||||
|
||||
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
|
||||
private static final int DURATION_US = 2_741_000;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
fail("Flac library not available.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
|
||||
throws IOException, FlacDecoderException, InterruptedException {
|
||||
byte[] data =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
||||
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
||||
decoderJni.setData(input);
|
||||
OutputFrameHolder outputFrameHolder = new OutputFrameHolder(ByteBuffer.allocateDirect(0));
|
||||
|
||||
FlacBinarySearchSeeker seeker =
|
||||
new FlacBinarySearchSeeker(
|
||||
decoderJni.decodeStreamMetadata(),
|
||||
/* firstFramePosition= */ 0,
|
||||
data.length,
|
||||
decoderJni,
|
||||
outputFrameHolder);
|
||||
SeekMap seekMap = seeker.getSeekMap();
|
||||
|
||||
assertThat(seekMap).isNotNull();
|
||||
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
|
||||
assertThat(seekMap.isSeekable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetSeekTargetUs_returnsSeekPending()
|
||||
throws IOException, FlacDecoderException, InterruptedException {
|
||||
byte[] data =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
||||
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
||||
decoderJni.setData(input);
|
||||
OutputFrameHolder outputFrameHolder = new OutputFrameHolder(ByteBuffer.allocateDirect(0));
|
||||
|
||||
FlacBinarySearchSeeker seeker =
|
||||
new FlacBinarySearchSeeker(
|
||||
decoderJni.decodeStreamMetadata(),
|
||||
/* firstFramePosition= */ 0,
|
||||
data.length,
|
||||
decoderJni,
|
||||
outputFrameHolder);
|
||||
seeker.setSeekTargetUs(/* timeUs= */ 1000);
|
||||
|
||||
assertThat(seeker.isSeeking()).isTrue();
|
||||
}
|
||||
}
|
@ -16,73 +16,43 @@
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Seeking tests for {@link FlacExtractor} when the FLAC stream does not have a SEEKTABLE. */
|
||||
/** Seeking tests for {@link FlacExtractor}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class FlacExtractorSeekTest {
|
||||
|
||||
private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac";
|
||||
private static final String TEST_FILE_SEEK_TABLE = "media/flac/bear.flac";
|
||||
private static final String TEST_FILE_BINARY_SEARCH = "media/flac/bear_one_metadata_block.flac";
|
||||
private static final String TEST_FILE_UNSEEKABLE =
|
||||
"media/flac/bear_no_seek_table_no_num_samples.flac";
|
||||
private static final int DURATION_US = 2_741_000;
|
||||
private static final Uri FILE_URI = Uri.parse("file:///android_asset/" + NO_SEEKTABLE_FLAC);
|
||||
private static final Random RANDOM = new Random(1234L);
|
||||
|
||||
private FakeExtractorOutput expectedOutput;
|
||||
private FakeTrackOutput expectedTrackOutput;
|
||||
|
||||
private DefaultDataSource dataSource;
|
||||
private PositionHolder positionHolder;
|
||||
private long totalInputLength;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
fail("Flac library not available.");
|
||||
}
|
||||
expectedOutput = new FakeExtractorOutput();
|
||||
extractAllSamplesFromFileToExpectedOutput(
|
||||
ApplicationProvider.getApplicationContext(), NO_SEEKTABLE_FLAC);
|
||||
expectedTrackOutput = expectedOutput.trackOutputs.get(0);
|
||||
|
||||
dataSource =
|
||||
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent")
|
||||
.createDataSource();
|
||||
totalInputLength = readInputLength();
|
||||
positionHolder = new PositionHolder();
|
||||
}
|
||||
private FlacExtractor extractor = new FlacExtractor();
|
||||
private FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
private DefaultDataSource dataSource =
|
||||
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()).createDataSource();
|
||||
|
||||
@Test
|
||||
public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
public void flacExtractorReads_seekTable_returnSeekableSeekMap() throws IOException {
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_SEEK_TABLE);
|
||||
|
||||
SeekMap seekMap = extractSeekMap(extractor, new FakeExtractorOutput());
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
|
||||
assertThat(seekMap).isNotNull();
|
||||
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
|
||||
@ -90,205 +60,227 @@ public final class FlacExtractorSeekTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
public void seeking_seekTable_handlesSeekToZero() throws IOException {
|
||||
String fileName = TEST_FILE_SEEK_TABLE;
|
||||
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long targetSeekTimeUs = 987_000;
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
long targetSeekTimeUs = 0;
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||
assertFirstFrameAfterSeekPrecedesTargetSeekTime(
|
||||
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
public void seeking_seekTable_handlesSeekToEoF() throws IOException {
|
||||
String fileName = TEST_FILE_SEEK_TABLE;
|
||||
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long targetSeekTimeUs = seekMap.getDurationUs();
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||
assertFirstFrameAfterSeekPrecedesTargetSeekTime(
|
||||
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
public void seeking_seekTable_handlesSeekingBackward() throws IOException {
|
||||
String fileName = TEST_FILE_SEEK_TABLE;
|
||||
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
long firstSeekTimeUs = 1_234_000;
|
||||
TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
long targetSeekTimeUs = 987_000;
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||
assertFirstFrameAfterSeekPrecedesTargetSeekTime(
|
||||
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seeking_seekTable_handlesSeekingForward() throws IOException {
|
||||
String fileName = TEST_FILE_SEEK_TABLE;
|
||||
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long firstSeekTimeUs = 987_000;
|
||||
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);
|
||||
TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
long targetSeekTimeUs = 1_234_000;
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||
assertFirstFrameAfterSeekPrecedesTargetSeekTime(
|
||||
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flacExtractorReads_binarySearch_returnSeekableSeekMap() throws IOException {
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_BINARY_SEARCH);
|
||||
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
|
||||
assertThat(seekMap).isNotNull();
|
||||
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
|
||||
assertThat(seekMap.isSeekable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seeking_binarySearch_handlesSeekToZero() throws IOException {
|
||||
String fileName = TEST_FILE_BINARY_SEARCH;
|
||||
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long targetSeekTimeUs = 0;
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
public void seeking_binarySearch_handlesSeekToEoF() throws IOException {
|
||||
String fileName = TEST_FILE_BINARY_SEARCH;
|
||||
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
long targetSeekTimeUs = seekMap.getDurationUs();
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seeking_binarySearch_handlesSeekingBackward() throws IOException {
|
||||
String fileName = TEST_FILE_BINARY_SEARCH;
|
||||
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long firstSeekTimeUs = 1_234_000;
|
||||
TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
long targetSeekTimeUs = 987_00;
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seeking_binarySearch_handlesSeekingForward() throws IOException {
|
||||
String fileName = TEST_FILE_BINARY_SEARCH;
|
||||
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
|
||||
long firstSeekTimeUs = 987_000;
|
||||
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);
|
||||
|
||||
TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
long targetSeekTimeUs = 1_234_000;
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
int extractedFrameIndex =
|
||||
TestUtil.seekToTimeUs(
|
||||
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()
|
||||
throws IOException, InterruptedException {
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
public void flacExtractorReads_unseekable_returnUnseekableSeekMap() throws IOException {
|
||||
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_UNSEEKABLE);
|
||||
|
||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||
|
||||
long numSeek = 100;
|
||||
for (long i = 0; i < numSeek; i++) {
|
||||
long targetSeekTimeUs = RANDOM.nextInt(DURATION_US + 1);
|
||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
||||
|
||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||
}
|
||||
assertThat(seekMap).isNotNull();
|
||||
assertThat(seekMap.getDurationUs()).isEqualTo(C.TIME_UNSET);
|
||||
assertThat(seekMap.isSeekable()).isFalse();
|
||||
}
|
||||
|
||||
// Internal methods
|
||||
private static void assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||
String fileName,
|
||||
FakeTrackOutput trackOutput,
|
||||
long targetSeekTimeUs,
|
||||
int firstFrameIndexAfterSeek)
|
||||
throws IOException {
|
||||
FakeTrackOutput expectedTrackOutput = getExpectedTrackOutput(fileName);
|
||||
int expectedFrameIndex = getFrameIndex(expectedTrackOutput, targetSeekTimeUs);
|
||||
|
||||
private long readInputLength() throws IOException {
|
||||
DataSpec dataSpec = new DataSpec(FILE_URI, 0, C.LENGTH_UNSET, null);
|
||||
long totalInputLength = dataSource.open(dataSpec);
|
||||
Util.closeQuietly(dataSource);
|
||||
return totalInputLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to the given seek time and keeps reading from input until we can extract at least one
|
||||
* frame from the seek position, or until end-of-input is reached.
|
||||
*
|
||||
* @return The index of the first extracted frame written to the given {@code trackOutput} after
|
||||
* the seek is completed, or -1 if the seek is completed without any extracted frame.
|
||||
*/
|
||||
private int seekToTimeUs(
|
||||
FlacExtractor flacExtractor, SeekMap seekMap, long seekTimeUs, FakeTrackOutput trackOutput)
|
||||
throws IOException, InterruptedException {
|
||||
int numSampleBeforeSeek = trackOutput.getSampleCount();
|
||||
SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);
|
||||
|
||||
long initialSeekLoadPosition = seekPoints.first.position;
|
||||
flacExtractor.seek(initialSeekLoadPosition, seekTimeUs);
|
||||
|
||||
positionHolder.position = C.POSITION_UNSET;
|
||||
ExtractorInput extractorInput = getExtractorInputFromPosition(initialSeekLoadPosition);
|
||||
int extractorReadResult = Extractor.RESULT_CONTINUE;
|
||||
while (true) {
|
||||
try {
|
||||
// Keep reading until we can read at least one frame after seek
|
||||
while (extractorReadResult == Extractor.RESULT_CONTINUE
|
||||
&& trackOutput.getSampleCount() == numSampleBeforeSeek) {
|
||||
extractorReadResult = flacExtractor.read(extractorInput, positionHolder);
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
|
||||
if (extractorReadResult == Extractor.RESULT_SEEK) {
|
||||
extractorInput = getExtractorInputFromPosition(positionHolder.position);
|
||||
extractorReadResult = Extractor.RESULT_CONTINUE;
|
||||
} else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT) {
|
||||
return -1;
|
||||
} else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {
|
||||
// First index after seek = num sample before seek.
|
||||
return numSampleBeforeSeek;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
|
||||
throws IOException, InterruptedException {
|
||||
try {
|
||||
ExtractorInput input = getExtractorInputFromPosition(0);
|
||||
extractor.init(output);
|
||||
while (output.seekMap == null) {
|
||||
extractor.read(input, positionHolder);
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
return output.seekMap;
|
||||
}
|
||||
|
||||
private void assertFirstFrameAfterSeekContainTargetSeekTime(
|
||||
FakeTrackOutput trackOutput, long seekTimeUs, int firstFrameIndexAfterSeek) {
|
||||
int expectedSampleIndex = findTargetFrameInExpectedOutput(seekTimeUs);
|
||||
// Assert that after seeking, the first sample frame written to output contains the sample
|
||||
// at seek time.
|
||||
trackOutput.assertSample(
|
||||
firstFrameIndexAfterSeek,
|
||||
expectedTrackOutput.getSampleData(expectedSampleIndex),
|
||||
expectedTrackOutput.getSampleTimeUs(expectedSampleIndex),
|
||||
expectedTrackOutput.getSampleFlags(expectedSampleIndex),
|
||||
expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));
|
||||
expectedTrackOutput.getSampleData(expectedFrameIndex),
|
||||
expectedTrackOutput.getSampleTimeUs(expectedFrameIndex),
|
||||
expectedTrackOutput.getSampleFlags(expectedFrameIndex),
|
||||
expectedTrackOutput.getSampleCryptoData(expectedFrameIndex));
|
||||
}
|
||||
|
||||
private int findTargetFrameInExpectedOutput(long seekTimeUs) {
|
||||
List<Long> sampleTimes = expectedTrackOutput.getSampleTimesUs();
|
||||
for (int i = 0; i < sampleTimes.size() - 1; i++) {
|
||||
long currentSampleTime = sampleTimes.get(i);
|
||||
long nextSampleTime = sampleTimes.get(i + 1);
|
||||
if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) {
|
||||
return i;
|
||||
private static void assertFirstFrameAfterSeekPrecedesTargetSeekTime(
|
||||
String fileName,
|
||||
FakeTrackOutput trackOutput,
|
||||
long targetSeekTimeUs,
|
||||
int firstFrameIndexAfterSeek)
|
||||
throws IOException {
|
||||
FakeTrackOutput expectedTrackOutput = getExpectedTrackOutput(fileName);
|
||||
int maxFrameIndex = getFrameIndex(expectedTrackOutput, targetSeekTimeUs);
|
||||
|
||||
long firstFrameAfterSeekTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek);
|
||||
assertThat(firstFrameAfterSeekTimeUs).isAtMost(targetSeekTimeUs);
|
||||
|
||||
boolean frameFound = false;
|
||||
for (int i = maxFrameIndex; i >= 0; i--) {
|
||||
if (firstFrameAfterSeekTimeUs == expectedTrackOutput.getSampleTimeUs(i)) {
|
||||
trackOutput.assertSample(
|
||||
firstFrameIndexAfterSeek,
|
||||
expectedTrackOutput.getSampleData(i),
|
||||
expectedTrackOutput.getSampleTimeUs(i),
|
||||
expectedTrackOutput.getSampleFlags(i),
|
||||
expectedTrackOutput.getSampleCryptoData(i));
|
||||
frameFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sampleTimes.size() - 1;
|
||||
|
||||
assertThat(frameFound).isTrue();
|
||||
}
|
||||
|
||||
private ExtractorInput getExtractorInputFromPosition(long position) throws IOException {
|
||||
DataSpec dataSpec = new DataSpec(FILE_URI, position, totalInputLength, /* key= */ null);
|
||||
dataSource.open(dataSpec);
|
||||
return new DefaultExtractorInput(dataSource, position, totalInputLength);
|
||||
private static FakeTrackOutput getExpectedTrackOutput(String fileName) throws IOException {
|
||||
return TestUtil.extractAllSamplesFromFile(
|
||||
new FlacExtractor(), ApplicationProvider.getApplicationContext(), fileName)
|
||||
.trackOutputs
|
||||
.get(0);
|
||||
}
|
||||
|
||||
private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName)
|
||||
throws IOException, InterruptedException {
|
||||
byte[] data = TestUtil.getByteArray(context, fileName);
|
||||
|
||||
FlacExtractor extractor = new FlacExtractor();
|
||||
extractor.init(expectedOutput);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
||||
|
||||
while (extractor.read(input, new PositionHolder()) != Extractor.RESULT_END_OF_INPUT) {}
|
||||
private static int getFrameIndex(FakeTrackOutput expectedTrackOutput, long targetSeekTimeUs) {
|
||||
List<Long> frameTimes = expectedTrackOutput.getSampleTimesUs();
|
||||
return Util.binarySearchFloor(
|
||||
frameTimes, targetSeekTimeUs, /* inclusive= */ true, /* stayInBounds= */ false);
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||
import org.junit.Before;
|
||||
@ -25,6 +24,8 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link FlacExtractor}. */
|
||||
// TODO(internal: b/26110951): Use org.junit.runners.Parameterized (and corresponding methods on
|
||||
// ExtractorAsserts) when it's supported by our testing infrastructure.
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FlacExtractorTest {
|
||||
|
||||
@ -36,14 +37,82 @@ public class FlacExtractorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractFlacSample() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(
|
||||
FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
|
||||
public void sample() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractFlacSampleWithId3Header() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(
|
||||
FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext());
|
||||
public void sampleWithId3HeaderAndId3Enabled() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear_with_id3.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_id3_enabled_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleWithId3HeaderAndId3Disabled() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
() -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA),
|
||||
/* file= */ "media/flac/bear_with_id3.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_id3_disabled_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleUnseekable() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear_no_seek_table_no_num_samples.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_no_seek_table_no_num_samples_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleWithVorbisComments() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear_with_vorbis_comments.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_vorbis_comments_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sampleWithPicture() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear_with_picture.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_picture_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oneMetadataBlock() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear_one_metadata_block.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_one_metadata_block_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noMinMaxFrameSize() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear_no_min_max_frame_size.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_no_min_max_frame_size_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noNumSamples() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear_no_num_samples.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_no_num_samples_raw");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uncommonSampleRate() throws Exception {
|
||||
ExtractorAsserts.assertAllBehaviors(
|
||||
FlacExtractor::new,
|
||||
/* file= */ "media/flac/bear_uncommon_sample_rate.flac",
|
||||
/* dumpFilesPrefix= */ "extractordumps/flac/bear_uncommon_sample_rate_raw");
|
||||
}
|
||||
}
|
||||
|
@ -20,10 +20,12 @@ import static org.junit.Assert.fail;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioSink;
|
||||
@ -32,6 +34,7 @@ import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.CapturingAudioSink;
|
||||
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@ -41,8 +44,8 @@ import org.junit.runner.RunWith;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FlacPlaybackTest {
|
||||
|
||||
private static final String BEAR_FLAC_16BIT = "bear-flac-16bit.mka";
|
||||
private static final String BEAR_FLAC_24BIT = "bear-flac-24bit.mka";
|
||||
private static final String BEAR_FLAC_16BIT = "mka/bear-flac-16bit.mka";
|
||||
private static final String BEAR_FLAC_24BIT = "mka/bear-flac-24bit.mka";
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
@ -68,7 +71,7 @@ public class FlacPlaybackTest {
|
||||
|
||||
TestPlaybackRunnable testPlaybackRunnable =
|
||||
new TestPlaybackRunnable(
|
||||
Uri.parse("asset:///" + fileName),
|
||||
Uri.parse("asset:///media/" + fileName),
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
audioSink);
|
||||
Thread thread = new Thread(testPlaybackRunnable);
|
||||
@ -78,8 +81,10 @@ public class FlacPlaybackTest {
|
||||
throw testPlaybackRunnable.playbackException;
|
||||
}
|
||||
|
||||
audioSink.assertOutput(
|
||||
ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump");
|
||||
DumpFileAsserts.assertOutput(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
audioSink,
|
||||
"audiosinkdumps/" + fileName + ".audiosink.dump");
|
||||
}
|
||||
|
||||
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||
@ -88,8 +93,8 @@ public class FlacPlaybackTest {
|
||||
private final Uri uri;
|
||||
private final AudioSink audioSink;
|
||||
|
||||
private ExoPlayer player;
|
||||
private ExoPlaybackException playbackException;
|
||||
@Nullable private ExoPlayer player;
|
||||
@Nullable private ExoPlaybackException playbackException;
|
||||
|
||||
public TestPlaybackRunnable(Uri uri, Context context, AudioSink audioSink) {
|
||||
this.uri = uri;
|
||||
@ -106,11 +111,11 @@ public class FlacPlaybackTest {
|
||||
player.addListener(this);
|
||||
MediaSource mediaSource =
|
||||
new ProgressiveMediaSource.Factory(
|
||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
|
||||
MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(uri);
|
||||
player.prepare(mediaSource);
|
||||
player.setPlayWhenReady(true);
|
||||
new DefaultDataSourceFactory(context), MatroskaExtractor.FACTORY)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
player.play();
|
||||
Looper.loop();
|
||||
}
|
||||
|
||||
@ -120,7 +125,7 @@ public class FlacPlaybackTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||
if (playbackState == Player.STATE_ENDED
|
||||
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
|
||||
player.release();
|
||||
|
@ -15,12 +15,14 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.FlacConstants;
|
||||
import com.google.android.exoplayer2.util.FlacStreamMetadata;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
@ -74,7 +76,7 @@ import java.nio.ByteBuffer;
|
||||
/* floorBytePosition= */ firstFramePosition,
|
||||
/* ceilingBytePosition= */ inputLength,
|
||||
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
|
||||
/* minimumSearchRange= */ Math.max(
|
||||
/* minimumSearchRange= */ max(
|
||||
FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize));
|
||||
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
||||
}
|
||||
@ -100,7 +102,7 @@ import java.nio.ByteBuffer;
|
||||
|
||||
@Override
|
||||
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleIndex)
|
||||
throws IOException, InterruptedException {
|
||||
throws IOException {
|
||||
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
|
||||
long searchPosition = input.getPosition();
|
||||
decoderJni.reset(searchPosition);
|
||||
|
@ -21,7 +21,7 @@ import com.google.android.exoplayer2.ParserException;
|
||||
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.util.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -63,7 +63,7 @@ import java.util.List;
|
||||
streamMetadata = decoderJni.decodeStreamMetadata();
|
||||
} catch (ParserException e) {
|
||||
throw new FlacDecoderException("Failed to decode StreamInfo", e);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
} catch (IOException e) {
|
||||
// Never happens.
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
@ -85,7 +85,7 @@ import java.util.List;
|
||||
|
||||
@Override
|
||||
protected SimpleOutputBuffer createOutputBuffer() {
|
||||
return new SimpleOutputBuffer(this);
|
||||
return new SimpleOutputBuffer(this::releaseOutputBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -107,7 +107,7 @@ import java.util.List;
|
||||
decoderJni.decodeSample(outputData);
|
||||
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||
return new FlacDecoderException("Frame decoding failed", e);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
} catch (IOException e) {
|
||||
// Never happens.
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
@ -15,12 +15,10 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import com.google.android.exoplayer2.audio.AudioDecoderException;
|
||||
import com.google.android.exoplayer2.decoder.DecoderException;
|
||||
|
||||
/**
|
||||
* Thrown when an Flac decoder error occurs.
|
||||
*/
|
||||
public final class FlacDecoderException extends AudioDecoderException {
|
||||
/** Thrown when an Flac decoder error occurs. */
|
||||
public final class FlacDecoderException extends DecoderException {
|
||||
|
||||
/* package */ FlacDecoderException(String message) {
|
||||
super(message);
|
||||
|
@ -15,13 +15,15 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.ext.flac;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||
import com.google.android.exoplayer2.util.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -51,12 +53,6 @@ import java.nio.ByteBuffer;
|
||||
@Nullable private byte[] tempBuffer;
|
||||
private boolean endOfExtractorInput;
|
||||
|
||||
// the constructor does not initialize fields: tempBuffer
|
||||
// call to flacInit() not allowed on the given receiver.
|
||||
@SuppressWarnings({
|
||||
"nullness:initialization.fields.uninitialized",
|
||||
"nullness:method.invocation.invalid"
|
||||
})
|
||||
public FlacDecoderJni() throws FlacDecoderException {
|
||||
if (!FlacLibrary.isAvailable()) {
|
||||
throw new FlacDecoderException("Failed to load decoder native libraries.");
|
||||
@ -121,10 +117,10 @@ import java.nio.ByteBuffer;
|
||||
* read from the source, then 0 is returned.
|
||||
*/
|
||||
@SuppressWarnings("unused") // Called from native code.
|
||||
public int read(ByteBuffer target) throws IOException, InterruptedException {
|
||||
public int read(ByteBuffer target) throws IOException {
|
||||
int byteCount = target.remaining();
|
||||
if (byteBufferData != null) {
|
||||
byteCount = Math.min(byteCount, byteBufferData.remaining());
|
||||
byteCount = min(byteCount, byteBufferData.remaining());
|
||||
int originalLimit = byteBufferData.limit();
|
||||
byteBufferData.limit(byteBufferData.position() + byteCount);
|
||||
target.put(byteBufferData);
|
||||
@ -132,7 +128,7 @@ import java.nio.ByteBuffer;
|
||||
} else if (extractorInput != null) {
|
||||
ExtractorInput extractorInput = this.extractorInput;
|
||||
byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
|
||||
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);
|
||||
byteCount = min(byteCount, TEMP_BUFFER_SIZE);
|
||||
int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
|
||||
if (read < 4) {
|
||||
// Reading less than 4 bytes, most of the time, happens because of getting the bytes left in
|
||||
@ -151,7 +147,7 @@ import java.nio.ByteBuffer;
|
||||
}
|
||||
|
||||
/** Decodes and consumes the metadata from the FLAC stream. */
|
||||
public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
|
||||
public FlacStreamMetadata decodeStreamMetadata() throws IOException {
|
||||
FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
|
||||
if (streamMetadata == null) {
|
||||
throw new ParserException("Failed to decode stream metadata");
|
||||
@ -167,7 +163,7 @@ import java.nio.ByteBuffer;
|
||||
* @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}.
|
||||
*/
|
||||
public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition)
|
||||
throws InterruptedException, IOException, FlacFrameDecodeException {
|
||||
throws IOException, FlacFrameDecodeException {
|
||||
try {
|
||||
decodeSample(output);
|
||||
} catch (IOException e) {
|
||||
@ -183,8 +179,7 @@ import java.nio.ByteBuffer;
|
||||
|
||||
/** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */
|
||||
@SuppressWarnings("ByteBufferBackingArray")
|
||||
public void decodeSample(ByteBuffer output)
|
||||
throws IOException, InterruptedException, FlacFrameDecodeException {
|
||||
public void decodeSample(ByteBuffer output) throws IOException, FlacFrameDecodeException {
|
||||
output.clear();
|
||||
int frameSize =
|
||||
output.isDirect()
|
||||
@ -272,8 +267,7 @@ import java.nio.ByteBuffer;
|
||||
}
|
||||
|
||||
private int readFromExtractorInput(
|
||||
ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)
|
||||
throws IOException, InterruptedException {
|
||||
ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length) throws IOException {
|
||||
int read = extractorInput.read(tempBuffer, offset, length);
|
||||
if (read == C.RESULT_END_OF_INPUT) {
|
||||
endOfExtractorInput = true;
|
||||
@ -284,14 +278,11 @@ import java.nio.ByteBuffer;
|
||||
|
||||
private native long flacInit();
|
||||
|
||||
private native FlacStreamMetadata flacDecodeMetadata(long context)
|
||||
throws IOException, InterruptedException;
|
||||
private native FlacStreamMetadata flacDecodeMetadata(long context) throws IOException;
|
||||
|
||||
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
|
||||
throws IOException, InterruptedException;
|
||||
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException;
|
||||
|
||||
private native int flacDecodeToArray(long context, byte[] outputArray)
|
||||
throws IOException, InterruptedException;
|
||||
private native int flacDecodeToArray(long context, byte[] outputArray) throws IOException;
|
||||
|
||||
private native long flacGetDecodePosition(long context);
|
||||
|
||||
|
@ -27,13 +27,13 @@ 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.FlacMetadataReader;
|
||||
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
|
||||
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.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.io.IOException;
|
||||
@ -53,6 +53,11 @@ public final class FlacExtractor implements Extractor {
|
||||
/** Factory that returns one extractor which is a {@link FlacExtractor}. */
|
||||
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
|
||||
|
||||
// LINT.IfChange
|
||||
/*
|
||||
* Flags in the two FLAC extractors should be kept in sync. If we ever change this then
|
||||
* DefaultExtractorsFactory will need modifying, because it currently assumes this is the case.
|
||||
*/
|
||||
/**
|
||||
* Flags controlling the behavior of the extractor. Possible flag value is {@link
|
||||
* #FLAG_DISABLE_ID3_METADATA}.
|
||||
@ -68,7 +73,9 @@ public final class FlacExtractor implements Extractor {
|
||||
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
|
||||
* required.
|
||||
*/
|
||||
public static final int FLAG_DISABLE_ID3_METADATA = 1;
|
||||
public static final int FLAG_DISABLE_ID3_METADATA =
|
||||
com.google.android.exoplayer2.extractor.flac.FlacExtractor.FLAG_DISABLE_ID3_METADATA;
|
||||
// LINT.ThenChange(../../../../../../../../../../../library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java)
|
||||
|
||||
private final ParsableByteArray outputBuffer;
|
||||
private final boolean id3MetadataDisabled;
|
||||
@ -113,14 +120,13 @@ public final class FlacExtractor implements Extractor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
||||
public boolean sniff(ExtractorInput input) throws IOException {
|
||||
id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
|
||||
return FlacMetadataReader.checkAndPeekStreamMarker(input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
||||
throws IOException, InterruptedException {
|
||||
public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException {
|
||||
if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
|
||||
id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true);
|
||||
}
|
||||
@ -185,7 +191,7 @@ public final class FlacExtractor implements Extractor {
|
||||
@RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
|
||||
@EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
|
||||
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
|
||||
private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
|
||||
private void decodeStreamMetadata(ExtractorInput input) throws IOException {
|
||||
if (streamMetadataDecoded) {
|
||||
return;
|
||||
}
|
||||
@ -204,7 +210,7 @@ public final class FlacExtractor implements Extractor {
|
||||
if (this.streamMetadata == null) {
|
||||
this.streamMetadata = streamMetadata;
|
||||
outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
|
||||
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
|
||||
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.getData()));
|
||||
binarySearchSeeker =
|
||||
outputSeekMap(
|
||||
flacDecoderJni,
|
||||
@ -225,7 +231,7 @@ public final class FlacExtractor implements Extractor {
|
||||
ParsableByteArray outputBuffer,
|
||||
OutputFrameHolder outputFrameHolder,
|
||||
TrackOutput trackOutput)
|
||||
throws InterruptedException, IOException {
|
||||
throws IOException {
|
||||
int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition);
|
||||
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
||||
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
||||
@ -250,7 +256,7 @@ public final class FlacExtractor implements Extractor {
|
||||
SeekMap seekMap;
|
||||
if (haveSeekTable) {
|
||||
seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni);
|
||||
} else if (streamLength != C.LENGTH_UNSET) {
|
||||
} else if (streamLength != C.LENGTH_UNSET && streamMetadata.totalSamples > 0) {
|
||||
long firstFramePosition = decoderJni.getDecodePosition();
|
||||
binarySearchSeeker =
|
||||
new FlacBinarySearchSeeker(
|
||||
@ -266,22 +272,16 @@ public final class FlacExtractor implements Extractor {
|
||||
private static void outputFormat(
|
||||
FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
|
||||
Format mediaFormat =
|
||||
Format.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
/* codecs= */ null,
|
||||
streamMetadata.getBitRate(),
|
||||
streamMetadata.getMaxDecodedFrameSize(),
|
||||
streamMetadata.channels,
|
||||
streamMetadata.sampleRate,
|
||||
getPcmEncoding(streamMetadata.bitsPerSample),
|
||||
/* encoderDelay= */ 0,
|
||||
/* encoderPadding= */ 0,
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null,
|
||||
metadata);
|
||||
new Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||
.setAverageBitrate(streamMetadata.getDecodedBitrate())
|
||||
.setPeakBitrate(streamMetadata.getDecodedBitrate())
|
||||
.setMaxInputSize(streamMetadata.getMaxDecodedFrameSize())
|
||||
.setChannelCount(streamMetadata.channels)
|
||||
.setSampleRate(streamMetadata.sampleRate)
|
||||
.setPcmEncoding(getPcmEncoding(streamMetadata.bitsPerSample))
|
||||
.setMetadata(metadata)
|
||||
.build();
|
||||
output.format(mediaFormat);
|
||||
}
|
||||
|
||||
|
@ -22,28 +22,27 @@ 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.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.audio.DecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.util.FlacConstants;
|
||||
import com.google.android.exoplayer2.util.FlacStreamMetadata;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Decodes and renders audio using the native Flac decoder. */
|
||||
public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
public final class LibflacAudioRenderer extends DecoderAudioRenderer<FlacDecoder> {
|
||||
|
||||
private static final String TAG = "LibflacAudioRenderer";
|
||||
private static final int NUM_BUFFERS = 16;
|
||||
|
||||
@MonotonicNonNull private FlacStreamMetadata streamMetadata;
|
||||
|
||||
public LibflacAudioRenderer() {
|
||||
this(/* eventHandler= */ null, /* eventListener= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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.
|
||||
@ -57,6 +56,8 @@ public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @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.
|
||||
@ -69,37 +70,40 @@ public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
super(
|
||||
eventHandler,
|
||||
eventListener,
|
||||
/* drmSessionManager= */ null,
|
||||
/* playClearSamplesWithoutKeys= */ false,
|
||||
audioSink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
@FormatSupport
|
||||
protected int supportsFormatInternal(
|
||||
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
if (!FlacLibrary.isAvailable()
|
||||
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
// Compute the PCM encoding that the FLAC decoder will output.
|
||||
@C.PcmEncoding int pcmEncoding;
|
||||
// Compute the format that the FLAC decoder will output.
|
||||
Format outputFormat;
|
||||
if (format.initializationData.isEmpty()) {
|
||||
// The initialization data might not be set if the format was obtained from a manifest (e.g.
|
||||
// for DASH playbacks) rather than directly from the media. In this case we assume
|
||||
// ENCODING_PCM_16BIT. If the actual encoding is different then playback will still succeed as
|
||||
// long as the AudioSink supports it, which will always be true when using DefaultAudioSink.
|
||||
pcmEncoding = C.ENCODING_PCM_16BIT;
|
||||
outputFormat =
|
||||
Util.getPcmFormat(C.ENCODING_PCM_16BIT, format.channelCount, format.sampleRate);
|
||||
} else {
|
||||
int streamMetadataOffset =
|
||||
FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE;
|
||||
FlacStreamMetadata streamMetadata =
|
||||
new FlacStreamMetadata(format.initializationData.get(0), streamMetadataOffset);
|
||||
pcmEncoding = Util.getPcmEncoding(streamMetadata.bitsPerSample);
|
||||
outputFormat = getOutputFormat(streamMetadata);
|
||||
}
|
||||
if (!supportsOutput(format.channelCount, pcmEncoding)) {
|
||||
if (!sinkSupportsFormat(outputFormat)) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
||||
} else if (format.exoMediaCryptoType != null) {
|
||||
return FORMAT_UNSUPPORTED_DRM;
|
||||
} else {
|
||||
return FORMAT_HANDLED;
|
||||
@ -109,27 +113,22 @@ public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
@Override
|
||||
protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||
throws FlacDecoderException {
|
||||
TraceUtil.beginSection("createFlacDecoder");
|
||||
FlacDecoder decoder =
|
||||
new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
|
||||
streamMetadata = decoder.getStreamMetadata();
|
||||
TraceUtil.endSection();
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Format getOutputFormat() {
|
||||
Assertions.checkNotNull(streamMetadata);
|
||||
return Format.createAudioSampleFormat(
|
||||
/* id= */ null,
|
||||
MimeTypes.AUDIO_RAW,
|
||||
/* codecs= */ null,
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
/* maxInputSize= */ Format.NO_VALUE,
|
||||
streamMetadata.channels,
|
||||
streamMetadata.sampleRate,
|
||||
protected Format getOutputFormat(FlacDecoder decoder) {
|
||||
return getOutputFormat(decoder.getStreamMetadata());
|
||||
}
|
||||
|
||||
private static Format getOutputFormat(FlacStreamMetadata streamMetadata) {
|
||||
return Util.getPcmFormat(
|
||||
Util.getPcmEncoding(streamMetadata.bitsPerSample),
|
||||
/* initializationData= */ null,
|
||||
/* drmInitData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
/* language= */ null);
|
||||
streamMetadata.channels,
|
||||
streamMetadata.sampleRate);
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
|
||||
context->parser->getStreamInfo();
|
||||
|
||||
jclass flacStreamMetadataClass = env->FindClass(
|
||||
"com/google/android/exoplayer2/util/"
|
||||
"com/google/android/exoplayer2/extractor/"
|
||||
"FlacStreamMetadata");
|
||||
jmethodID flacStreamMetadataConstructor =
|
||||
env->GetMethodID(flacStreamMetadataClass, "<init>",
|
||||
|
@ -26,7 +26,7 @@ import org.junit.runner.RunWith;
|
||||
public final class DefaultRenderersFactoryTest {
|
||||
|
||||
@Test
|
||||
public void createRenderers_instantiatesVpxRenderer() {
|
||||
public void createRenderers_instantiatesFlacRenderer() {
|
||||
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
|
||||
LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO);
|
||||
}
|
||||
|
@ -11,24 +11,9 @@
|
||||
// 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'
|
||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
}
|
||||
android.defaultConfig.minSdkVersion 19
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
@ -36,6 +21,7 @@ dependencies {
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
api 'com.google.vr:sdk-base:1.190.0'
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -32,7 +32,7 @@ import java.nio.ByteOrder;
|
||||
* href="https://github.com/google/ExoPlayer/issues">issue tracker</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class GvrAudioProcessor implements AudioProcessor {
|
||||
public class GvrAudioProcessor implements AudioProcessor {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.gvr");
|
||||
|
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.
|
||||
*/
|
||||
@NonNullApi
|
||||
package com.google.android.exoplayer2.ext.gvr;
|
||||
|
||||
import com.google.android.exoplayer2.util.NonNullApi;
|
@ -26,39 +26,35 @@ 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 `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. Note that the IMA
|
||||
To use the extension, follow the instructions on the
|
||||
[Ad insertion page](https://exoplayer.dev/ad-insertion.html#declarative-ad-support)
|
||||
of the developer guide. The `AdsLoaderProvider` passed to the player's
|
||||
`DefaultMediaSourceFactory` should return an `ImaAdsLoader`. Note that the IMA
|
||||
extension only supports players which are accessed on the application's main
|
||||
thread.
|
||||
|
||||
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
|
||||
`AdsMediaSource`. It is also important to persist the player position when
|
||||
background, and are recreated when returning 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`. When re-entering the foreground, pass the
|
||||
same instance back when `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called
|
||||
to restore the state. 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.
|
||||
playback 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. 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
|
||||
You can try the IMA extension in the ExoPlayer demo app, which has test content
|
||||
in the "IMA sample ad tags" section of the sample chooser. The demo app's
|
||||
`PlayerActivity` also shows how to persist the `ImaAdsLoader` instance and the
|
||||
player position when backgrounded during ad playback.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [ExoPlayer documentation on ad insertion][]
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
|
||||
belong to this module.
|
||||
|
||||
[ExoPlayer documentation on ad insertion]: https://exoplayer.dev/ad-insertion.html
|
||||
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user