mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Merge branch 'dev-v2' into cancel-hls-chunk-download-and-discard-upstream
# Conflicts: # library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
This commit is contained in:
commit
069fa69c4b
1571 changed files with 105125 additions and 36924 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -58,6 +58,7 @@ extensions/vp9/src/main/jni/libvpx_android_configs
|
||||||
extensions/vp9/src/main/jni/libyuv
|
extensions/vp9/src/main/jni/libyuv
|
||||||
|
|
||||||
# AV1 extension
|
# AV1 extension
|
||||||
|
extensions/av1/src/main/jni/cpu_features
|
||||||
extensions/av1/src/main/jni/libgav1
|
extensions/av1/src/main/jni/libgav1
|
||||||
|
|
||||||
# Opus extension
|
# Opus extension
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ bazel-testlogs
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
dist
|
dist
|
||||||
|
jacoco.exec
|
||||||
tmp
|
tmp
|
||||||
|
|
||||||
# VP9 extension
|
# VP9 extension
|
||||||
|
|
|
||||||
4349
RELEASENOTES.md
4349
RELEASENOTES.md
File diff suppressed because it is too large
Load diff
|
|
@ -17,9 +17,9 @@ buildscript {
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||||
classpath 'com.novoda:bintray-release:0.9.1'
|
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 {
|
allprojects {
|
||||||
|
|
|
||||||
|
|
@ -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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|
@ -13,20 +13,20 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.11.0'
|
releaseVersion = '2.11.4'
|
||||||
releaseVersionCode = 2011000
|
releaseVersionCode = 2011004
|
||||||
minSdkVersion = 16
|
minSdkVersion = 16
|
||||||
appTargetSdkVersion = 29
|
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
|
compileSdkVersion = 29
|
||||||
dexmakerVersion = '2.21.0'
|
dexmakerVersion = '2.21.0'
|
||||||
|
junitVersion = '4.13-rc-2'
|
||||||
|
guavaVersion = '28.2-android'
|
||||||
mockitoVersion = '2.25.0'
|
mockitoVersion = '2.25.0'
|
||||||
robolectricVersion = '4.3'
|
robolectricVersion = '4.3.1'
|
||||||
autoValueVersion = '1.6'
|
|
||||||
autoServiceVersion = '1.0-rc4'
|
|
||||||
checkerframeworkVersion = '2.5.0'
|
checkerframeworkVersion = '2.5.0'
|
||||||
jsr305Version = '3.0.2'
|
jsr305Version = '3.0.2'
|
||||||
kotlinAnnotationsVersion = '1.3.31'
|
kotlinAnnotationsVersion = '1.3.70'
|
||||||
androidxAnnotationVersion = '1.1.0'
|
androidxAnnotationVersion = '1.1.0'
|
||||||
androidxAppCompatVersion = '1.1.0'
|
androidxAppCompatVersion = '1.1.0'
|
||||||
androidxCollectionVersion = '1.1.0'
|
androidxCollectionVersion = '1.1.0'
|
||||||
|
|
@ -35,7 +35,7 @@ project.ext {
|
||||||
androidxTestJUnitVersion = '1.1.1'
|
androidxTestJUnitVersion = '1.1.1'
|
||||||
androidxTestRunnerVersion = '1.2.0'
|
androidxTestRunnerVersion = '1.2.0'
|
||||||
androidxTestRulesVersion = '1.2.0'
|
androidxTestRulesVersion = '1.2.0'
|
||||||
truthVersion = '0.44'
|
truthVersion = '1.0'
|
||||||
modulePrefix = ':'
|
modulePrefix = ':'
|
||||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,15 @@ if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||||
}
|
}
|
||||||
|
|
||||||
include modulePrefix + 'library'
|
include modulePrefix + 'library'
|
||||||
|
include modulePrefix + 'library-common'
|
||||||
include modulePrefix + 'library-core'
|
include modulePrefix + 'library-core'
|
||||||
include modulePrefix + 'library-dash'
|
include modulePrefix + 'library-dash'
|
||||||
|
include modulePrefix + 'library-extractor'
|
||||||
include modulePrefix + 'library-hls'
|
include modulePrefix + 'library-hls'
|
||||||
include modulePrefix + 'library-smoothstreaming'
|
include modulePrefix + 'library-smoothstreaming'
|
||||||
include modulePrefix + 'library-ui'
|
include modulePrefix + 'library-ui'
|
||||||
include modulePrefix + 'testutils'
|
include modulePrefix + 'testutils'
|
||||||
|
include modulePrefix + 'testdata'
|
||||||
include modulePrefix + 'extension-av1'
|
include modulePrefix + 'extension-av1'
|
||||||
include modulePrefix + 'extension-ffmpeg'
|
include modulePrefix + 'extension-ffmpeg'
|
||||||
include modulePrefix + 'extension-flac'
|
include modulePrefix + 'extension-flac'
|
||||||
|
|
@ -41,12 +44,15 @@ include modulePrefix + 'extension-jobdispatcher'
|
||||||
include modulePrefix + 'extension-workmanager'
|
include modulePrefix + 'extension-workmanager'
|
||||||
|
|
||||||
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
|
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-core').projectDir = new File(rootDir, 'library/core')
|
||||||
project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')
|
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-hls').projectDir = new File(rootDir, 'library/hls')
|
||||||
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
|
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
|
||||||
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
|
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
|
||||||
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
|
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-av1').projectDir = new File(rootDir, 'extensions/av1')
|
||||||
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
|
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
|
||||||
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
|
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,8 @@ dependencies {
|
||||||
implementation project(modulePrefix + 'library-ui')
|
implementation project(modulePrefix + 'library-ui')
|
||||||
implementation project(modulePrefix + 'extension-cast')
|
implementation project(modulePrefix + 'extension-cast')
|
||||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
|
||||||
<uses-sdk/>
|
<uses-sdk/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
|
import com.google.android.exoplayer2.MediaMetadata;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
@ -42,19 +42,19 @@ import java.util.List;
|
||||||
samples.add(
|
samples.add(
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
|
.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)
|
.setMimeType(MIME_TYPE_DASH)
|
||||||
.build());
|
.build());
|
||||||
samples.add(
|
samples.add(
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
|
.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)
|
.setMimeType(MIME_TYPE_HLS)
|
||||||
.build());
|
.build());
|
||||||
samples.add(
|
samples.add(
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setUri("https://html5demos.com/assets/dizzy.mp4")
|
.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)
|
.setMimeType(MIME_TYPE_VIDEO_MP4)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
|
|
@ -62,39 +62,29 @@ import java.util.List;
|
||||||
samples.add(
|
samples.add(
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"))
|
.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)
|
.setMimeType(MIME_TYPE_DASH)
|
||||||
.setDrmConfiguration(
|
.setDrmUuid(C.WIDEVINE_UUID)
|
||||||
new DrmConfiguration(
|
.setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||||
C.WIDEVINE_UUID,
|
|
||||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
|
||||||
Collections.emptyMap()))
|
|
||||||
.build());
|
.build());
|
||||||
samples.add(
|
samples.add(
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setUri(
|
.setUri("https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd")
|
||||||
Uri.parse(
|
.setMediaMetadata(
|
||||||
"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"))
|
new MediaMetadata.Builder().setTitle("Widevine DASH cbc1: Tears").build())
|
||||||
.setTitle("Widevine DASH cbc1: Tears")
|
|
||||||
.setMimeType(MIME_TYPE_DASH)
|
.setMimeType(MIME_TYPE_DASH)
|
||||||
.setDrmConfiguration(
|
.setDrmUuid(C.WIDEVINE_UUID)
|
||||||
new DrmConfiguration(
|
.setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||||
C.WIDEVINE_UUID,
|
|
||||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
|
||||||
Collections.emptyMap()))
|
|
||||||
.build());
|
.build());
|
||||||
samples.add(
|
samples.add(
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setUri(
|
.setUri("https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd")
|
||||||
Uri.parse(
|
.setMediaMetadata(
|
||||||
"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"))
|
new MediaMetadata.Builder().setTitle("Widevine DASH cbcs: Tears").build())
|
||||||
.setTitle("Widevine DASH cbcs: Tears")
|
|
||||||
.setMimeType(MIME_TYPE_DASH)
|
.setMimeType(MIME_TYPE_DASH)
|
||||||
.setDrmConfiguration(
|
.setDrmUuid(C.WIDEVINE_UUID)
|
||||||
new DrmConfiguration(
|
.setDrmLicenseUri("https://proxy.uat.widevine.com/proxy?provider=widevine_test")
|
||||||
C.WIDEVINE_UUID,
|
|
||||||
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
|
||||||
Collections.emptyMap()))
|
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
SAMPLES = Collections.unmodifiableList(samples);
|
SAMPLES = Collections.unmodifiableList(samples);
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,12 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
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.PlayerControlView;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
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.CastButtonFactory;
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
import com.google.android.gms.dynamite.DynamiteModule;
|
import com.google.android.gms.dynamite.DynamiteModule;
|
||||||
|
|
@ -171,8 +173,6 @@ public class MainActivity extends AppCompatActivity
|
||||||
showToast(R.string.error_unsupported_audio);
|
showToast(R.string.error_unsupported_audio);
|
||||||
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||||
showToast(R.string.error_unsupported_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> {
|
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@NonNull
|
||||||
public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
TextView v = (TextView) LayoutInflater.from(parent.getContext())
|
TextView v = (TextView) LayoutInflater.from(parent.getContext())
|
||||||
.inflate(android.R.layout.simple_list_item_1, parent, false);
|
.inflate(android.R.layout.simple_list_item_1, parent, false);
|
||||||
|
|
@ -207,9 +208,10 @@ public class MainActivity extends AppCompatActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
||||||
holder.item = playerManager.getItem(position);
|
holder.item = Assertions.checkNotNull(playerManager.getItem(position));
|
||||||
|
|
||||||
TextView view = holder.textView;
|
TextView view = holder.textView;
|
||||||
view.setText(holder.item.title);
|
view.setText(holder.item.mediaMetadata.title);
|
||||||
// TODO: Solve coloring using the theme's ColorStateList.
|
// TODO: Solve coloring using the theme's ColorStateList.
|
||||||
view.setTextColor(
|
view.setTextColor(
|
||||||
ColorUtils.setAlphaComponent(
|
ColorUtils.setAlphaComponent(
|
||||||
|
|
@ -236,7 +238,9 @@ public class MainActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin,
|
public boolean onMove(
|
||||||
|
@NonNull RecyclerView list,
|
||||||
|
RecyclerView.ViewHolder origin,
|
||||||
RecyclerView.ViewHolder target) {
|
RecyclerView.ViewHolder target) {
|
||||||
int fromPosition = origin.getAdapterPosition();
|
int fromPosition = origin.getAdapterPosition();
|
||||||
int toPosition = target.getAdapterPosition();
|
int toPosition = target.getAdapterPosition();
|
||||||
|
|
@ -261,7 +265,7 @@ public class MainActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
|
public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) {
|
||||||
super.clearView(recyclerView, viewHolder);
|
super.clearView(recyclerView, viewHolder);
|
||||||
if (draggingFromPosition != C.INDEX_UNSET) {
|
if (draggingFromPosition != C.INDEX_UNSET) {
|
||||||
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||||
|
|
@ -300,11 +304,11 @@ public class MainActivity extends AppCompatActivity
|
||||||
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
|
@NonNull
|
||||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||||
View view = super.getView(position, convertView, 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;
|
return view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,45 +16,29 @@
|
||||||
package com.google.android.exoplayer2.castdemo;
|
package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||||
import com.google.android.exoplayer2.Player.EventListener;
|
import com.google.android.exoplayer2.Player.EventListener;
|
||||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
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.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.ext.cast.SessionAvailabilityListener;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||||
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.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.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
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 com.google.android.gms.cast.framework.CastContext;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/** Manages players and an internal media queue for the demo app. */
|
/** Manages players and an internal media queue for the demo app. */
|
||||||
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
|
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
|
||||||
|
|
@ -77,6 +61,7 @@ import java.util.Map;
|
||||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
new DefaultHttpDataSourceFactory(USER_AGENT);
|
||||||
|
|
||||||
|
private final DefaultMediaSourceFactory defaultMediaSourceFactory;
|
||||||
private final PlayerView localPlayerView;
|
private final PlayerView localPlayerView;
|
||||||
private final PlayerControlView castControlView;
|
private final PlayerControlView castControlView;
|
||||||
private final DefaultTrackSelector trackSelector;
|
private final DefaultTrackSelector trackSelector;
|
||||||
|
|
@ -84,8 +69,6 @@ import java.util.Map;
|
||||||
private final CastPlayer castPlayer;
|
private final CastPlayer castPlayer;
|
||||||
private final ArrayList<MediaItem> mediaQueue;
|
private final ArrayList<MediaItem> mediaQueue;
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
|
||||||
private final MediaItemConverter mediaItemConverter;
|
|
||||||
|
|
||||||
private TrackGroupArray lastSeenTrackGroupArray;
|
private TrackGroupArray lastSeenTrackGroupArray;
|
||||||
private int currentItemIndex;
|
private int currentItemIndex;
|
||||||
|
|
@ -111,11 +94,10 @@ import java.util.Map;
|
||||||
this.castControlView = castControlView;
|
this.castControlView = castControlView;
|
||||||
mediaQueue = new ArrayList<>();
|
mediaQueue = new ArrayList<>();
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
|
||||||
mediaItemConverter = new DefaultMediaItemConverter();
|
|
||||||
|
|
||||||
trackSelector = new DefaultTrackSelector(context);
|
trackSelector = new DefaultTrackSelector(context);
|
||||||
exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
|
exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
|
||||||
|
defaultMediaSourceFactory = DefaultMediaSourceFactory.newInstance(context, DATA_SOURCE_FACTORY);
|
||||||
exoPlayer.addListener(this);
|
exoPlayer.addListener(this);
|
||||||
localPlayerView.setPlayer(exoPlayer);
|
localPlayerView.setPlayer(exoPlayer);
|
||||||
|
|
||||||
|
|
@ -135,7 +117,7 @@ import java.util.Map;
|
||||||
* @param itemIndex The index of the item to play.
|
* @param itemIndex The index of the item to play.
|
||||||
*/
|
*/
|
||||||
public void selectQueueItem(int itemIndex) {
|
public void selectQueueItem(int itemIndex) {
|
||||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
setCurrentItem(itemIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the index of the currently played item. */
|
/** Returns the index of the currently played item. */
|
||||||
|
|
@ -150,10 +132,7 @@ import java.util.Map;
|
||||||
*/
|
*/
|
||||||
public void addItem(MediaItem item) {
|
public void addItem(MediaItem item) {
|
||||||
mediaQueue.add(item);
|
mediaQueue.add(item);
|
||||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
currentPlayer.addMediaItem(item);
|
||||||
if (currentPlayer == castPlayer) {
|
|
||||||
castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the size of the media queue. */
|
/** Returns the size of the media queue. */
|
||||||
|
|
@ -182,16 +161,7 @@ import java.util.Map;
|
||||||
if (itemIndex == -1) {
|
if (itemIndex == -1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
currentPlayer.removeMediaItem(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediaQueue.remove(itemIndex);
|
mediaQueue.remove(itemIndex);
|
||||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
||||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
||||||
|
|
@ -205,34 +175,25 @@ import java.util.Map;
|
||||||
* Moves an item within the queue.
|
* Moves an item within the queue.
|
||||||
*
|
*
|
||||||
* @param item The item to move.
|
* @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.
|
* @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);
|
int fromIndex = mediaQueue.indexOf(item);
|
||||||
if (fromIndex == -1) {
|
if (fromIndex == -1) {
|
||||||
return false;
|
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.
|
// Index update.
|
||||||
if (fromIndex == currentItemIndex) {
|
if (fromIndex == currentItemIndex) {
|
||||||
maybeSetCurrentItemAndNotify(toIndex);
|
maybeSetCurrentItemAndNotify(newIndex);
|
||||||
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
} else if (fromIndex < currentItemIndex && newIndex >= currentItemIndex) {
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||||
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
} else if (fromIndex > currentItemIndex && newIndex <= currentItemIndex) {
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,7 +218,6 @@ import java.util.Map;
|
||||||
public void release() {
|
public void release() {
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
mediaQueue.clear();
|
mediaQueue.clear();
|
||||||
concatenatingMediaSource.clear();
|
|
||||||
castPlayer.setSessionAvailabilityListener(null);
|
castPlayer.setSessionAvailabilityListener(null);
|
||||||
castPlayer.release();
|
castPlayer.release();
|
||||||
localPlayerView.setPlayer(null);
|
localPlayerView.setPlayer(null);
|
||||||
|
|
@ -267,7 +227,7 @@ import java.util.Map;
|
||||||
// Player.EventListener implementation.
|
// Player.EventListener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||||
updateCurrentItemIndex();
|
updateCurrentItemIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,12 +237,13 @@ import java.util.Map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
public void onTimelineChanged(@NonNull Timeline timeline, @TimelineChangeReason int reason) {
|
||||||
updateCurrentItemIndex();
|
updateCurrentItemIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
public void onTracksChanged(
|
||||||
|
@NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) {
|
||||||
if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) {
|
if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) {
|
||||||
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
|
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
|
||||||
trackSelector.getCurrentMappedTrackInfo();
|
trackSelector.getCurrentMappedTrackInfo();
|
||||||
|
|
@ -360,35 +321,26 @@ import java.util.Map;
|
||||||
this.currentPlayer = currentPlayer;
|
this.currentPlayer = currentPlayer;
|
||||||
|
|
||||||
// Media queue management.
|
// Media queue management.
|
||||||
if (currentPlayer == exoPlayer) {
|
currentPlayer.setMediaItems(mediaQueue, windowIndex, playbackPositionMs);
|
||||||
exoPlayer.prepare(concatenatingMediaSource);
|
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||||
}
|
currentPlayer.prepare();
|
||||||
|
|
||||||
// Playback transition.
|
|
||||||
if (windowIndex != C.INDEX_UNSET) {
|
|
||||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 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);
|
maybeSetCurrentItemAndNotify(itemIndex);
|
||||||
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
if (currentPlayer.getCurrentTimeline().getWindowCount() != mediaQueue.size()) {
|
||||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
// This only happens with the cast player. The receiver app in the cast device clears the
|
||||||
for (int i = 0; i < items.length; i++) {
|
// timeline when the last item of the timeline has been played to end.
|
||||||
items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
|
currentPlayer.setMediaItems(mediaQueue, itemIndex, C.TIME_UNSET);
|
||||||
}
|
|
||||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
|
||||||
} else {
|
} else {
|
||||||
currentPlayer.seekTo(itemIndex, positionMs);
|
currentPlayer.seekTo(itemIndex, C.TIME_UNSET);
|
||||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
|
||||||
}
|
}
|
||||||
|
currentPlayer.setPlayWhenReady(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
||||||
|
|
@ -398,62 +350,4 @@ import java.util.Map;
|
||||||
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
demos/gl/README.md
Normal file
11
demos/gl/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# ExoPlayer GL demo
|
||||||
|
|
||||||
|
This app demonstrates how to render video to a [GLSurfaceView][] while applying
|
||||||
|
a GL shader.
|
||||||
|
|
||||||
|
The shader shows an overlap bitmap on top of the video. The overlay bitmap is
|
||||||
|
drawn using an Android canvas, and includes the current frame's presentation
|
||||||
|
timestamp, to show how to get the timestamp of the frame currently in the
|
||||||
|
off-screen surface texture.
|
||||||
|
|
||||||
|
[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView
|
||||||
53
demos/gl/build.gradle
Normal file
53
demos/gl/build.gradle
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
// 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: '../../constants.gradle'
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
versionName project.ext.releaseVersion
|
||||||
|
versionCode project.ext.releaseVersionCode
|
||||||
|
minSdkVersion project.ext.minSdkVersion
|
||||||
|
targetSdkVersion project.ext.appTargetSdkVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
shrinkResources true
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
// This demo app does not have translations.
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(modulePrefix + 'library-core')
|
||||||
|
implementation project(modulePrefix + 'library-ui')
|
||||||
|
implementation project(modulePrefix + 'library-dash')
|
||||||
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||||
|
}
|
||||||
50
demos/gl/src/main/AndroidManifest.xml
Normal file
50
demos/gl/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.google.android.exoplayer2.gldemo">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<uses-sdk/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/application_name">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.android.exoplayer.gldemo.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
<data android:scheme="https"/>
|
||||||
|
<data android:scheme="content"/>
|
||||||
|
<data android:scheme="asset"/>
|
||||||
|
<data android:scheme="file"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#extension GL_OES_EGL_image_external : require
|
||||||
|
precision mediump float;
|
||||||
|
// External texture containing video decoder output.
|
||||||
|
uniform samplerExternalOES tex_sampler_0;
|
||||||
|
// Texture containing the overlap bitmap.
|
||||||
|
uniform sampler2D tex_sampler_1;
|
||||||
|
// Horizontal scaling factor for the overlap bitmap.
|
||||||
|
uniform float scaleX;
|
||||||
|
// Vertical scaling factory for the overlap bitmap.
|
||||||
|
uniform float scaleY;
|
||||||
|
varying vec2 v_texcoord;
|
||||||
|
void main() {
|
||||||
|
vec4 videoColor = texture2D(tex_sampler_0, v_texcoord);
|
||||||
|
vec4 overlayColor = texture2D(tex_sampler_1,
|
||||||
|
vec2(v_texcoord.x * scaleX,
|
||||||
|
v_texcoord.y * scaleY));
|
||||||
|
// Blend the video decoder output and the overlay bitmap.
|
||||||
|
gl_FragColor = videoColor * (1.0 - overlayColor.a)
|
||||||
|
+ overlayColor * overlayColor.a;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
// 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.
|
||||||
|
attribute vec4 a_position;
|
||||||
|
attribute vec3 a_texcoord;
|
||||||
|
varying vec2 v_texcoord;
|
||||||
|
void main() {
|
||||||
|
gl_Position = a_position;
|
||||||
|
v_texcoord = a_texcoord.xy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.gldemo;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.opengl.GLES20;
|
||||||
|
import android.opengl.GLUtils;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.GlUtil;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Locale;
|
||||||
|
import javax.microedition.khronos.opengles.GL10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Video processor that demonstrates how to overlay a bitmap on video output using a GL shader. The
|
||||||
|
* bitmap is drawn using an Android {@link Canvas}.
|
||||||
|
*/
|
||||||
|
/* package */ final class BitmapOverlayVideoProcessor
|
||||||
|
implements VideoProcessingGLSurfaceView.VideoProcessor {
|
||||||
|
|
||||||
|
private static final int OVERLAY_WIDTH = 512;
|
||||||
|
private static final int OVERLAY_HEIGHT = 256;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final Paint paint;
|
||||||
|
private final int[] textures;
|
||||||
|
private final Bitmap overlayBitmap;
|
||||||
|
private final Bitmap logoBitmap;
|
||||||
|
private final Canvas overlayCanvas;
|
||||||
|
|
||||||
|
private int program;
|
||||||
|
@Nullable private GlUtil.Attribute[] attributes;
|
||||||
|
@Nullable private GlUtil.Uniform[] uniforms;
|
||||||
|
|
||||||
|
private float bitmapScaleX;
|
||||||
|
private float bitmapScaleY;
|
||||||
|
|
||||||
|
public BitmapOverlayVideoProcessor(Context context) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
paint = new Paint();
|
||||||
|
paint.setTextSize(64);
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
|
||||||
|
textures = new int[1];
|
||||||
|
overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888);
|
||||||
|
overlayCanvas = new Canvas(overlayBitmap);
|
||||||
|
try {
|
||||||
|
logoBitmap =
|
||||||
|
((BitmapDrawable)
|
||||||
|
context.getPackageManager().getApplicationIcon(context.getPackageName()))
|
||||||
|
.getBitmap();
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
String vertexShaderCode =
|
||||||
|
loadAssetAsString(context, "bitmap_overlay_video_processor_vertex.glsl");
|
||||||
|
String fragmentShaderCode =
|
||||||
|
loadAssetAsString(context, "bitmap_overlay_video_processor_fragment.glsl");
|
||||||
|
program = GlUtil.compileProgram(vertexShaderCode, fragmentShaderCode);
|
||||||
|
GlUtil.Attribute[] attributes = GlUtil.getAttributes(program);
|
||||||
|
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);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.attributes = attributes;
|
||||||
|
this.uniforms = uniforms;
|
||||||
|
GLES20.glGenTextures(1, textures, 0);
|
||||||
|
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||||
|
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
|
||||||
|
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
|
||||||
|
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
|
||||||
|
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);
|
||||||
|
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSurfaceSize(int width, int height) {
|
||||||
|
bitmapScaleX = (float) width / OVERLAY_WIDTH;
|
||||||
|
bitmapScaleY = (float) height / OVERLAY_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(int frameTexture, long frameTimestampUs) {
|
||||||
|
// Draw to the canvas and store it in a texture.
|
||||||
|
String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND);
|
||||||
|
overlayBitmap.eraseColor(Color.TRANSPARENT);
|
||||||
|
overlayCanvas.drawBitmap(logoBitmap, /* left= */ 32, /* top= */ 32, paint);
|
||||||
|
overlayCanvas.drawText(text, /* x= */ 200, /* y= */ 130, paint);
|
||||||
|
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||||
|
GLUtils.texSubImage2D(
|
||||||
|
GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap);
|
||||||
|
GlUtil.checkGlError();
|
||||||
|
|
||||||
|
// Run the shader program.
|
||||||
|
GlUtil.Uniform[] uniforms = Assertions.checkNotNull(this.uniforms);
|
||||||
|
GlUtil.Attribute[] attributes = Assertions.checkNotNull(this.attributes);
|
||||||
|
GLES20.glUseProgram(program);
|
||||||
|
for (GlUtil.Uniform uniform : uniforms) {
|
||||||
|
switch (uniform.name) {
|
||||||
|
case "tex_sampler_0":
|
||||||
|
uniform.setSamplerTexId(frameTexture, /* unit= */ 0);
|
||||||
|
break;
|
||||||
|
case "tex_sampler_1":
|
||||||
|
uniform.setSamplerTexId(textures[0], /* unit= */ 1);
|
||||||
|
break;
|
||||||
|
case "scaleX":
|
||||||
|
uniform.setFloat(bitmapScaleX);
|
||||||
|
break;
|
||||||
|
case "scaleY":
|
||||||
|
uniform.setFloat(bitmapScaleY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (GlUtil.Attribute copyExternalAttribute : attributes) {
|
||||||
|
copyExternalAttribute.bind();
|
||||||
|
}
|
||||||
|
for (GlUtil.Uniform copyExternalUniform : uniforms) {
|
||||||
|
copyExternalUniform.bind();
|
||||||
|
}
|
||||||
|
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||||
|
GlUtil.checkGlError();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String loadAssetAsString(Context context, String assetFileName) {
|
||||||
|
@Nullable InputStream inputStream = null;
|
||||||
|
try {
|
||||||
|
inputStream = context.getAssets().open(assetFileName);
|
||||||
|
return Util.fromUtf8Bytes(Util.toByteArray(inputStream));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
} finally {
|
||||||
|
Util.closeQuietly(inputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.gldemo;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||||
|
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
|
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.HttpDataSource;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.EventLogger;
|
||||||
|
import com.google.android.exoplayer2.util.GlUtil;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity that demonstrates playback of video to an {@link android.opengl.GLSurfaceView} with
|
||||||
|
* postprocessing of the video content using GL.
|
||||||
|
*/
|
||||||
|
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";
|
||||||
|
|
||||||
|
private static final String ACTION_VIEW = "com.google.android.exoplayer.gldemo.action.VIEW";
|
||||||
|
private static final String EXTENSION_EXTRA = "extension";
|
||||||
|
private static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||||
|
private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||||
|
|
||||||
|
@Nullable private PlayerView playerView;
|
||||||
|
@Nullable private VideoProcessingGLSurfaceView videoProcessingGLSurfaceView;
|
||||||
|
|
||||||
|
@Nullable private SimpleExoPlayer player;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.main_activity);
|
||||||
|
playerView = findViewById(R.id.player_view);
|
||||||
|
|
||||||
|
Context context = getApplicationContext();
|
||||||
|
boolean requestSecureSurface = getIntent().hasExtra(DRM_SCHEME_EXTRA);
|
||||||
|
if (requestSecureSurface && !GlUtil.isProtectedContentExtensionSupported(context)) {
|
||||||
|
Toast.makeText(
|
||||||
|
context, R.string.error_protected_content_extension_not_supported, Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
|
||||||
|
new VideoProcessingGLSurfaceView(
|
||||||
|
context, requestSecureSurface, new BitmapOverlayVideoProcessor(context));
|
||||||
|
FrameLayout contentFrame = findViewById(R.id.exo_content_frame);
|
||||||
|
contentFrame.addView(videoProcessingGLSurfaceView);
|
||||||
|
this.videoProcessingGLSurfaceView = videoProcessingGLSurfaceView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
if (Util.SDK_INT > 23) {
|
||||||
|
initializePlayer();
|
||||||
|
if (playerView != null) {
|
||||||
|
playerView.onResume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (Util.SDK_INT <= 23 || player == null) {
|
||||||
|
initializePlayer();
|
||||||
|
if (playerView != null) {
|
||||||
|
playerView.onResume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if (Util.SDK_INT <= 23) {
|
||||||
|
if (playerView != null) {
|
||||||
|
playerView.onPause();
|
||||||
|
}
|
||||||
|
releasePlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
if (Util.SDK_INT > 23) {
|
||||||
|
if (playerView != null) {
|
||||||
|
playerView.onPause();
|
||||||
|
}
|
||||||
|
releasePlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializePlayer() {
|
||||||
|
Intent intent = getIntent();
|
||||||
|
String action = intent.getAction();
|
||||||
|
Uri uri =
|
||||||
|
ACTION_VIEW.equals(action)
|
||||||
|
? Assertions.checkNotNull(intent.getData())
|
||||||
|
: Uri.parse(DEFAULT_MEDIA_URI);
|
||||||
|
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
|
||||||
|
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);
|
||||||
|
HttpMediaDrmCallback drmCallback =
|
||||||
|
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
|
||||||
|
drmSessionManager =
|
||||||
|
new DefaultDrmSessionManager.Builder()
|
||||||
|
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
||||||
|
.build(drmCallback);
|
||||||
|
} else {
|
||||||
|
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
DataSource.Factory dataSourceFactory =
|
||||||
|
new DefaultDataSourceFactory(
|
||||||
|
this, Util.getUserAgent(this, getString(R.string.application_name)));
|
||||||
|
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);
|
||||||
|
} else if (type == C.TYPE_OTHER) {
|
||||||
|
mediaSource =
|
||||||
|
new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
|
||||||
|
player.setRepeatMode(Player.REPEAT_MODE_ALL);
|
||||||
|
player.setMediaSource(mediaSource);
|
||||||
|
player.prepare();
|
||||||
|
player.play();
|
||||||
|
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
|
||||||
|
Assertions.checkNotNull(this.videoProcessingGLSurfaceView);
|
||||||
|
videoProcessingGLSurfaceView.setVideoComponent(
|
||||||
|
Assertions.checkNotNull(player.getVideoComponent()));
|
||||||
|
Assertions.checkNotNull(playerView).setPlayer(player);
|
||||||
|
player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null));
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releasePlayer() {
|
||||||
|
Assertions.checkNotNull(playerView).setPlayer(null);
|
||||||
|
if (player != null) {
|
||||||
|
player.release();
|
||||||
|
Assertions.checkNotNull(videoProcessingGLSurfaceView).setVideoComponent(null);
|
||||||
|
player = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.gldemo;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.media.MediaFormat;
|
||||||
|
import android.opengl.EGL14;
|
||||||
|
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;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.GlUtil;
|
||||||
|
import com.google.android.exoplayer2.util.TimedValueQueue;
|
||||||
|
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import javax.microedition.khronos.egl.EGL10;
|
||||||
|
import javax.microedition.khronos.egl.EGLConfig;
|
||||||
|
import javax.microedition.khronos.egl.EGLContext;
|
||||||
|
import javax.microedition.khronos.egl.EGLDisplay;
|
||||||
|
import javax.microedition.khronos.egl.EGLSurface;
|
||||||
|
import javax.microedition.khronos.opengles.GL10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link GLSurfaceView} that creates a GL context (optionally for protected content) and passes
|
||||||
|
* video frames to a {@link VideoProcessor} for drawing to the view.
|
||||||
|
*
|
||||||
|
* <p>This view must be created programmatically, as it is necessary to specify whether a context
|
||||||
|
* supporting protected content should be created at construction time.
|
||||||
|
*/
|
||||||
|
public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
||||||
|
|
||||||
|
/** Processes video frames, provided via a GL texture. */
|
||||||
|
public interface VideoProcessor {
|
||||||
|
/** Performs any required GL initialization. */
|
||||||
|
void initialize();
|
||||||
|
|
||||||
|
/** Sets the size of the output surface in pixels. */
|
||||||
|
void setSurfaceSize(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws using GL operations.
|
||||||
|
*
|
||||||
|
* @param frameTexture The ID of a GL texture containing a video frame.
|
||||||
|
* @param frameTimestampUs The presentation timestamp of the frame, in microseconds.
|
||||||
|
*/
|
||||||
|
void draw(int frameTexture, long frameTimestampUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
|
||||||
|
|
||||||
|
private final VideoRenderer renderer;
|
||||||
|
private final Handler mainHandler;
|
||||||
|
|
||||||
|
@Nullable private SurfaceTexture surfaceTexture;
|
||||||
|
@Nullable private Surface surface;
|
||||||
|
@Nullable private Player.VideoComponent videoComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance. Pass {@code true} for {@code requireSecureContext} if the {@link
|
||||||
|
* GLSurfaceView GLSurfaceView's} associated GL context should handle secure content (if the
|
||||||
|
* device supports it).
|
||||||
|
*
|
||||||
|
* @param context The {@link Context}.
|
||||||
|
* @param requireSecureContext Whether a GL context supporting protected content should be
|
||||||
|
* 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);
|
||||||
|
renderer = new VideoRenderer(videoProcessor);
|
||||||
|
mainHandler = new Handler();
|
||||||
|
setEGLContextClientVersion(2);
|
||||||
|
setEGLConfigChooser(
|
||||||
|
/* redSize= */ 8,
|
||||||
|
/* greenSize= */ 8,
|
||||||
|
/* blueSize= */ 8,
|
||||||
|
/* alphaSize= */ 8,
|
||||||
|
/* depthSize= */ 0,
|
||||||
|
/* stencilSize= */ 0);
|
||||||
|
setEGLContextFactory(
|
||||||
|
new EGLContextFactory() {
|
||||||
|
@Override
|
||||||
|
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
|
||||||
|
int[] glAttributes;
|
||||||
|
if (requireSecureContext) {
|
||||||
|
glAttributes =
|
||||||
|
new int[] {
|
||||||
|
EGL14.EGL_CONTEXT_CLIENT_VERSION,
|
||||||
|
2,
|
||||||
|
EGL_PROTECTED_CONTENT_EXT,
|
||||||
|
EGL14.EGL_TRUE,
|
||||||
|
EGL14.EGL_NONE
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
|
||||||
|
}
|
||||||
|
return egl.eglCreateContext(
|
||||||
|
display, eglConfig, /* share_context= */ EGL10.EGL_NO_CONTEXT, glAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
|
||||||
|
egl.eglDestroyContext(display, context);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setEGLWindowSurfaceFactory(
|
||||||
|
new EGLWindowSurfaceFactory() {
|
||||||
|
@Override
|
||||||
|
public EGLSurface createWindowSurface(
|
||||||
|
EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) {
|
||||||
|
int[] attribsList =
|
||||||
|
requireSecureContext
|
||||||
|
? new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE}
|
||||||
|
: new int[] {EGL10.EGL_NONE};
|
||||||
|
return egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
|
||||||
|
egl.eglDestroySurface(display, surface);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setRenderer(renderer);
|
||||||
|
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches or detaches (if {@code newVideoComponent} is {@code null}) this view from the video
|
||||||
|
* component of the player.
|
||||||
|
*
|
||||||
|
* @param newVideoComponent The new video component, or {@code null} to detach this view.
|
||||||
|
*/
|
||||||
|
public void setVideoComponent(@Nullable Player.VideoComponent newVideoComponent) {
|
||||||
|
if (newVideoComponent == videoComponent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (videoComponent != null) {
|
||||||
|
if (surface != null) {
|
||||||
|
videoComponent.clearVideoSurface(surface);
|
||||||
|
}
|
||||||
|
videoComponent.clearVideoFrameMetadataListener(renderer);
|
||||||
|
}
|
||||||
|
videoComponent = newVideoComponent;
|
||||||
|
if (videoComponent != null) {
|
||||||
|
videoComponent.setVideoFrameMetadataListener(renderer);
|
||||||
|
videoComponent.setVideoSurface(surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow();
|
||||||
|
// Post to make sure we occur in order with any onSurfaceTextureAvailable calls.
|
||||||
|
mainHandler.post(
|
||||||
|
() -> {
|
||||||
|
if (surface != null) {
|
||||||
|
if (videoComponent != null) {
|
||||||
|
videoComponent.setVideoSurface(null);
|
||||||
|
}
|
||||||
|
releaseSurface(surfaceTexture, surface);
|
||||||
|
surfaceTexture = null;
|
||||||
|
surface = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
|
||||||
|
mainHandler.post(
|
||||||
|
() -> {
|
||||||
|
SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
|
||||||
|
Surface oldSurface = VideoProcessingGLSurfaceView.this.surface;
|
||||||
|
this.surfaceTexture = surfaceTexture;
|
||||||
|
this.surface = new Surface(surfaceTexture);
|
||||||
|
releaseSurface(oldSurfaceTexture, oldSurface);
|
||||||
|
if (videoComponent != null) {
|
||||||
|
videoComponent.setVideoSurface(surface);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void releaseSurface(
|
||||||
|
@Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
|
||||||
|
if (oldSurfaceTexture != null) {
|
||||||
|
oldSurfaceTexture.release();
|
||||||
|
}
|
||||||
|
if (oldSurface != null) {
|
||||||
|
oldSurface.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class VideoRenderer implements GLSurfaceView.Renderer, VideoFrameMetadataListener {
|
||||||
|
|
||||||
|
private final VideoProcessor videoProcessor;
|
||||||
|
private final AtomicBoolean frameAvailable;
|
||||||
|
private final TimedValueQueue<Long> sampleTimestampQueue;
|
||||||
|
|
||||||
|
private int texture;
|
||||||
|
@Nullable private SurfaceTexture surfaceTexture;
|
||||||
|
|
||||||
|
private boolean initialized;
|
||||||
|
private int width;
|
||||||
|
private int height;
|
||||||
|
private long frameTimestampUs;
|
||||||
|
|
||||||
|
public VideoRenderer(VideoProcessor videoProcessor) {
|
||||||
|
this.videoProcessor = videoProcessor;
|
||||||
|
frameAvailable = new AtomicBoolean();
|
||||||
|
sampleTimestampQueue = new TimedValueQueue<>();
|
||||||
|
width = -1;
|
||||||
|
height = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||||
|
texture = GlUtil.createExternalTexture();
|
||||||
|
surfaceTexture = new SurfaceTexture(texture);
|
||||||
|
surfaceTexture.setOnFrameAvailableListener(
|
||||||
|
surfaceTexture -> {
|
||||||
|
frameAvailable.set(true);
|
||||||
|
requestRender();
|
||||||
|
});
|
||||||
|
onSurfaceTextureAvailable(surfaceTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||||
|
GLES20.glViewport(0, 0, width, height);
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDrawFrame(GL10 gl) {
|
||||||
|
if (videoProcessor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
videoProcessor.initialize();
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width != -1 && height != -1) {
|
||||||
|
videoProcessor.setSurfaceSize(width, height);
|
||||||
|
width = -1;
|
||||||
|
height = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameAvailable.compareAndSet(true, false)) {
|
||||||
|
SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture);
|
||||||
|
surfaceTexture.updateTexImage();
|
||||||
|
long lastFrameTimestampNs = surfaceTexture.getTimestamp();
|
||||||
|
Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs);
|
||||||
|
if (frameTimestampUs != null) {
|
||||||
|
this.frameTimestampUs = frameTimestampUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
videoProcessor.draw(texture, frameTimestampUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onVideoFrameAboutToBeRendered(
|
||||||
|
long presentationTimeUs,
|
||||||
|
long releaseTimeNs,
|
||||||
|
@NonNull Format format,
|
||||||
|
@Nullable MediaFormat mediaFormat) {
|
||||||
|
sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
demos/gl/src/main/res/layout/main_activity.xml
Normal file
30
demos/gl/src/main/res/layout/main_activity.xml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:keepScreenOn="true">
|
||||||
|
|
||||||
|
<com.google.android.exoplayer2.ui.PlayerView
|
||||||
|
android:id="@+id/player_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:surface_type="none"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
BIN
demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
22
demos/gl/src/main/res/values/strings.xml
Normal file
22
demos/gl/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="application_name">ExoPlayer GL demo</string>
|
||||||
|
|
||||||
|
<string name="error_protected_content_extension_not_supported">The GL protected content extension is not supported.</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
|
|
@ -64,7 +64,7 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-dash')
|
implementation project(modulePrefix + 'library-dash')
|
||||||
implementation project(modulePrefix + 'library-hls')
|
implementation project(modulePrefix + 'library-hls')
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,13 @@
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
||||||
"drm_scheme": "widevine",
|
"drm_scheme": "widevine",
|
||||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||||
|
"drm_session_for_clear_types": ["audio", "video"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -609,5 +616,30 @@
|
||||||
"subtitle_language": "en"
|
"subtitle_language": "en"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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_url": "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_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -22,17 +22,14 @@ import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||||
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
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.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.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
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.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.cache.Cache;
|
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
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.NoOpCacheEvictor;
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
|
@ -45,6 +42,8 @@ import java.io.IOException;
|
||||||
*/
|
*/
|
||||||
public class DemoApplication extends Application {
|
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 TAG = "DemoApplication";
|
||||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||||
|
|
@ -57,6 +56,7 @@ public class DemoApplication extends Application {
|
||||||
private Cache downloadCache;
|
private Cache downloadCache;
|
||||||
private DownloadManager downloadManager;
|
private DownloadManager downloadManager;
|
||||||
private DownloadTracker downloadTracker;
|
private DownloadTracker downloadTracker;
|
||||||
|
private DownloadNotificationHelper downloadNotificationHelper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
|
@ -93,6 +93,14 @@ public class DemoApplication extends Application {
|
||||||
.setExtensionRendererMode(extensionRendererMode);
|
.setExtensionRendererMode(extensionRendererMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DownloadNotificationHelper getDownloadNotificationHelper() {
|
||||||
|
if (downloadNotificationHelper == null) {
|
||||||
|
downloadNotificationHelper =
|
||||||
|
new DownloadNotificationHelper(this, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||||
|
}
|
||||||
|
return downloadNotificationHelper;
|
||||||
|
}
|
||||||
|
|
||||||
public DownloadManager getDownloadManager() {
|
public DownloadManager getDownloadManager() {
|
||||||
initDownloadManager();
|
initDownloadManager();
|
||||||
return downloadManager;
|
return downloadManager;
|
||||||
|
|
@ -119,11 +127,9 @@ public class DemoApplication extends Application {
|
||||||
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||||
upgradeActionFile(
|
upgradeActionFile(
|
||||||
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
|
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
|
||||||
DownloaderConstructorHelper downloaderConstructorHelper =
|
|
||||||
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
|
||||||
downloadManager =
|
downloadManager =
|
||||||
new DownloadManager(
|
new DownloadManager(
|
||||||
this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
|
this, getDatabaseProvider(), getDownloadCache(), buildHttpDataSourceFactory());
|
||||||
downloadTracker =
|
downloadTracker =
|
||||||
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
|
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
|
||||||
}
|
}
|
||||||
|
|
@ -160,14 +166,12 @@ public class DemoApplication extends Application {
|
||||||
return downloadDirectory;
|
return downloadDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
|
protected static CacheDataSource.Factory buildReadOnlyCacheDataSource(
|
||||||
DataSource.Factory upstreamFactory, Cache cache) {
|
DataSource.Factory upstreamFactory, Cache cache) {
|
||||||
return new CacheDataSourceFactory(
|
return new CacheDataSource.Factory()
|
||||||
cache,
|
.setCache(cache)
|
||||||
upstreamFactory,
|
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||||
new FileDataSource.Factory(),
|
.setCacheWriteDataSinkFactory(null)
|
||||||
/* cacheWriteDataSinkFactory= */ null,
|
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
|
||||||
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
|
|
||||||
/* eventListener= */ null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,11 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.demo.DemoApplication.DOWNLOAD_NOTIFICATION_CHANNEL_ID;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import com.google.android.exoplayer2.offline.Download;
|
import com.google.android.exoplayer2.offline.Download;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
|
|
@ -28,33 +32,31 @@ import java.util.List;
|
||||||
/** A service for downloading media. */
|
/** A service for downloading media. */
|
||||||
public class DemoDownloadService extends DownloadService {
|
public class DemoDownloadService extends DownloadService {
|
||||||
|
|
||||||
private static final String CHANNEL_ID = "download_channel";
|
|
||||||
private static final int JOB_ID = 1;
|
private static final int JOB_ID = 1;
|
||||||
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
|
||||||
|
|
||||||
private DownloadNotificationHelper notificationHelper;
|
|
||||||
|
|
||||||
public DemoDownloadService() {
|
public DemoDownloadService() {
|
||||||
super(
|
super(
|
||||||
FOREGROUND_NOTIFICATION_ID,
|
FOREGROUND_NOTIFICATION_ID,
|
||||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||||
CHANNEL_ID,
|
DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
||||||
R.string.exo_download_notification_channel_name,
|
R.string.exo_download_notification_channel_name,
|
||||||
/* channelDescriptionResourceId= */ 0);
|
/* channelDescriptionResourceId= */ 0);
|
||||||
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@NonNull
|
||||||
protected DownloadManager getDownloadManager() {
|
protected DownloadManager getDownloadManager() {
|
||||||
return ((DemoApplication) getApplication()).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();
|
||||||
|
DownloadNotificationHelper downloadNotificationHelper =
|
||||||
|
application.getDownloadNotificationHelper();
|
||||||
|
downloadManager.addListener(
|
||||||
|
new TerminalStateNotificationHelper(
|
||||||
|
this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1));
|
||||||
|
return downloadManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -63,29 +65,53 @@ public class DemoDownloadService extends DownloadService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Notification getForegroundNotification(List<Download> downloads) {
|
@NonNull
|
||||||
return notificationHelper.buildProgressNotification(
|
protected Notification getForegroundNotification(@NonNull List<Download> downloads) {
|
||||||
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
|
return ((DemoApplication) getApplication())
|
||||||
|
.getDownloadNotificationHelper()
|
||||||
|
.buildProgressNotification(
|
||||||
|
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
protected void onDownloadChanged(Download download) {
|
* Creates and displays notifications for downloads when they complete or fail.
|
||||||
Notification notification;
|
*
|
||||||
if (download.state == Download.STATE_COMPLETED) {
|
* <p>This helper will outlive the lifespan of a single instance of {@link DemoDownloadService}.
|
||||||
notification =
|
* It is static to avoid leaking the first {@link DemoDownloadService} instance.
|
||||||
notificationHelper.buildDownloadCompletedNotification(
|
*/
|
||||||
R.drawable.ic_download_done,
|
private static final class TerminalStateNotificationHelper implements DownloadManager.Listener {
|
||||||
/* contentIntent= */ null,
|
|
||||||
Util.fromUtf8Bytes(download.request.data));
|
private final Context context;
|
||||||
} else if (download.state == Download.STATE_FAILED) {
|
private final DownloadNotificationHelper notificationHelper;
|
||||||
notification =
|
|
||||||
notificationHelper.buildDownloadFailedNotification(
|
private int nextNotificationId;
|
||||||
R.drawable.ic_download_done,
|
|
||||||
/* contentIntent= */ null,
|
public TerminalStateNotificationHelper(
|
||||||
Util.fromUtf8Bytes(download.request.data));
|
Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) {
|
||||||
} else {
|
this.context = context.getApplicationContext();
|
||||||
return;
|
this.notificationHelper = notificationHelper;
|
||||||
|
nextNotificationId = firstNotificationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDownloadChanged(@NonNull DownloadManager manager, @NonNull Download download) {
|
||||||
|
Notification notification;
|
||||||
|
if (download.state == Download.STATE_COMPLETED) {
|
||||||
|
notification =
|
||||||
|
notificationHelper.buildDownloadCompletedNotification(
|
||||||
|
R.drawable.ic_download_done,
|
||||||
|
/* contentIntent= */ null,
|
||||||
|
Util.fromUtf8Bytes(download.request.data));
|
||||||
|
} else if (download.state == Download.STATE_FAILED) {
|
||||||
|
notification =
|
||||||
|
notificationHelper.buildDownloadFailedNotification(
|
||||||
|
R.drawable.ic_download_done,
|
||||||
|
/* contentIntent= */ null,
|
||||||
|
Util.fromUtf8Bytes(download.request.data));
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NotificationUtil.setNotification(context, nextNotificationId++, notification);
|
||||||
}
|
}
|
||||||
NotificationUtil.setNotification(this, nextNotificationId++, notification);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,17 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.offline.Download;
|
import com.google.android.exoplayer2.offline.Download;
|
||||||
import com.google.android.exoplayer2.offline.DownloadCursor;
|
import com.google.android.exoplayer2.offline.DownloadCursor;
|
||||||
|
|
@ -80,8 +84,8 @@ public class DownloadTracker {
|
||||||
listeners.remove(listener);
|
listeners.remove(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDownloaded(Uri uri) {
|
public boolean isDownloaded(MediaItem mediaItem) {
|
||||||
Download download = downloads.get(uri);
|
Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
||||||
return download != null && download.state != Download.STATE_FAILED;
|
return download != null && download.state != Download.STATE_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,12 +95,8 @@ public class DownloadTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleDownload(
|
public void toggleDownload(
|
||||||
FragmentManager fragmentManager,
|
FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) {
|
||||||
String name,
|
Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
||||||
Uri uri,
|
|
||||||
String extension,
|
|
||||||
RenderersFactory renderersFactory) {
|
|
||||||
Download download = downloads.get(uri);
|
|
||||||
if (download != null) {
|
if (download != null) {
|
||||||
DownloadService.sendRemoveDownload(
|
DownloadService.sendRemoveDownload(
|
||||||
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
|
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
|
||||||
|
|
@ -106,7 +106,7 @@ public class DownloadTracker {
|
||||||
}
|
}
|
||||||
startDownloadDialogHelper =
|
startDownloadDialogHelper =
|
||||||
new StartDownloadDialogHelper(
|
new StartDownloadDialogHelper(
|
||||||
fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
|
fragmentManager, getDownloadHelper(mediaItem, renderersFactory), mediaItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,18 +121,23 @@ public class DownloadTracker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DownloadHelper getDownloadHelper(
|
private DownloadHelper getDownloadHelper(MediaItem mediaItem, RenderersFactory renderersFactory) {
|
||||||
Uri uri, String extension, RenderersFactory renderersFactory) {
|
MediaItem.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties);
|
||||||
int type = Util.inferContentType(uri, extension);
|
@C.ContentType
|
||||||
|
int type =
|
||||||
|
Util.inferContentTypeWithMimeType(playbackProperties.uri, playbackProperties.mimeType);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
|
return DownloadHelper.forDash(
|
||||||
|
context, playbackProperties.uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
|
return DownloadHelper.forSmoothStreaming(
|
||||||
|
context, playbackProperties.uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
|
return DownloadHelper.forHls(
|
||||||
|
context, playbackProperties.uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return DownloadHelper.forProgressive(context, uri);
|
return DownloadHelper.forProgressive(context, playbackProperties.uri);
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +146,8 @@ public class DownloadTracker {
|
||||||
private class DownloadManagerListener implements DownloadManager.Listener {
|
private class DownloadManagerListener implements DownloadManager.Listener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDownloadChanged(DownloadManager downloadManager, Download download) {
|
public void onDownloadChanged(
|
||||||
|
@NonNull DownloadManager downloadManager, @NonNull Download download) {
|
||||||
downloads.put(download.request.uri, download);
|
downloads.put(download.request.uri, download);
|
||||||
for (Listener listener : listeners) {
|
for (Listener listener : listeners) {
|
||||||
listener.onDownloadsChanged();
|
listener.onDownloadsChanged();
|
||||||
|
|
@ -149,7 +155,8 @@ public class DownloadTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDownloadRemoved(DownloadManager downloadManager, Download download) {
|
public void onDownloadRemoved(
|
||||||
|
@NonNull DownloadManager downloadManager, @NonNull Download download) {
|
||||||
downloads.remove(download.request.uri);
|
downloads.remove(download.request.uri);
|
||||||
for (Listener listener : listeners) {
|
for (Listener listener : listeners) {
|
||||||
listener.onDownloadsChanged();
|
listener.onDownloadsChanged();
|
||||||
|
|
@ -164,16 +171,16 @@ public class DownloadTracker {
|
||||||
|
|
||||||
private final FragmentManager fragmentManager;
|
private final FragmentManager fragmentManager;
|
||||||
private final DownloadHelper downloadHelper;
|
private final DownloadHelper downloadHelper;
|
||||||
private final String name;
|
private final MediaItem mediaItem;
|
||||||
|
|
||||||
private TrackSelectionDialog trackSelectionDialog;
|
private TrackSelectionDialog trackSelectionDialog;
|
||||||
private MappedTrackInfo mappedTrackInfo;
|
private MappedTrackInfo mappedTrackInfo;
|
||||||
|
|
||||||
public StartDownloadDialogHelper(
|
public StartDownloadDialogHelper(
|
||||||
FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
|
FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) {
|
||||||
this.fragmentManager = fragmentManager;
|
this.fragmentManager = fragmentManager;
|
||||||
this.downloadHelper = downloadHelper;
|
this.downloadHelper = downloadHelper;
|
||||||
this.name = name;
|
this.mediaItem = mediaItem;
|
||||||
downloadHelper.prepare(this);
|
downloadHelper.prepare(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,7 +194,7 @@ public class DownloadTracker {
|
||||||
// DownloadHelper.Callback implementation.
|
// DownloadHelper.Callback implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepared(DownloadHelper helper) {
|
public void onPrepared(@NonNull DownloadHelper helper) {
|
||||||
if (helper.getPeriodCount() == 0) {
|
if (helper.getPeriodCount() == 0) {
|
||||||
Log.d(TAG, "No periods found. Downloading entire stream.");
|
Log.d(TAG, "No periods found. Downloading entire stream.");
|
||||||
startDownload();
|
startDownload();
|
||||||
|
|
@ -214,7 +221,7 @@ public class DownloadTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareError(DownloadHelper helper, IOException e) {
|
public void onPrepareError(@NonNull DownloadHelper helper, @NonNull IOException e) {
|
||||||
Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
|
Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
|
||||||
Log.e(
|
Log.e(
|
||||||
TAG,
|
TAG,
|
||||||
|
|
@ -268,7 +275,8 @@ public class DownloadTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
private DownloadRequest buildDownloadRequest() {
|
private DownloadRequest buildDownloadRequest() {
|
||||||
return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
|
return downloadHelper.getDownloadRequest(
|
||||||
|
Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
/*
|
||||||
|
* 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.offline.DownloadRequest;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Util to read from and populate an intent. */
|
||||||
|
public class IntentUtil {
|
||||||
|
|
||||||
|
/** A tag to hold custom playback configuration attributes. */
|
||||||
|
public static class Tag {
|
||||||
|
|
||||||
|
/** Whether the stream is a live stream. */
|
||||||
|
public final boolean isLive;
|
||||||
|
/** The spherical stereo mode or null. */
|
||||||
|
@Nullable public final String sphericalStereoMode;
|
||||||
|
|
||||||
|
/** Creates an instance. */
|
||||||
|
public Tag(boolean isLive, @Nullable String sphericalStereoMode) {
|
||||||
|
this.isLive = isLive;
|
||||||
|
this.sphericalStereoMode = sphericalStereoMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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";
|
||||||
|
|
||||||
|
// 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 IS_LIVE_EXTRA = "is_live";
|
||||||
|
public static final String MIME_TYPE_EXTRA = "mime_type";
|
||||||
|
// For backwards compatibility only.
|
||||||
|
public static final String EXTENSION_EXTRA = "extension";
|
||||||
|
|
||||||
|
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_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types";
|
||||||
|
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||||
|
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";
|
||||||
|
|
||||||
|
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||||
|
public static final String TUNNELING_EXTRA = "tunneling";
|
||||||
|
|
||||||
|
/** Creates a list of {@link MediaItem media items} from an {@link Intent}. */
|
||||||
|
public static List<MediaItem> createMediaItemsFromIntent(
|
||||||
|
Intent intent, DownloadTracker downloadTracker) {
|
||||||
|
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,
|
||||||
|
downloadTracker.getDownloadRequest(uri)));
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Uri uri = intent.getData();
|
||||||
|
mediaItems.add(
|
||||||
|
createMediaItemFromIntent(
|
||||||
|
uri, intent, /* extrasKeySuffix= */ "", downloadTracker.getDownloadRequest(uri)));
|
||||||
|
}
|
||||||
|
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.PlaybackProperties playbackProperties =
|
||||||
|
checkNotNull(mediaItems.get(0).playbackProperties);
|
||||||
|
intent.setAction(IntentUtil.ACTION_VIEW).setData(playbackProperties.uri);
|
||||||
|
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "");
|
||||||
|
} else {
|
||||||
|
intent.setAction(IntentUtil.ACTION_VIEW_LIST);
|
||||||
|
for (int i = 0; i < mediaItems.size(); i++) {
|
||||||
|
MediaItem.PlaybackProperties playbackProperties =
|
||||||
|
checkNotNull(mediaItems.get(i).playbackProperties);
|
||||||
|
intent.putExtra(IntentUtil.URI_EXTRA + ("_" + i), playbackProperties.uri.toString());
|
||||||
|
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "_" + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Makes a best guess to infer the MIME type from a {@link Uri} and an optional extension. */
|
||||||
|
@Nullable
|
||||||
|
public static String inferAdaptiveStreamMimeType(Uri uri, @Nullable String extension) {
|
||||||
|
@C.ContentType int contentType = Util.inferContentType(uri, extension);
|
||||||
|
switch (contentType) {
|
||||||
|
case C.TYPE_DASH:
|
||||||
|
return MimeTypes.APPLICATION_MPD;
|
||||||
|
case C.TYPE_HLS:
|
||||||
|
return MimeTypes.APPLICATION_M3U8;
|
||||||
|
case C.TYPE_SS:
|
||||||
|
return MimeTypes.APPLICATION_SS;
|
||||||
|
case C.TYPE_OTHER:
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaItem createMediaItemFromIntent(
|
||||||
|
Uri uri, Intent intent, String extrasKeySuffix, @Nullable DownloadRequest downloadRequest) {
|
||||||
|
String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
|
||||||
|
if (mimeType == null) {
|
||||||
|
// Try to use extension for backwards compatibility.
|
||||||
|
String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
|
||||||
|
mimeType = inferAdaptiveStreamMimeType(uri, extension);
|
||||||
|
}
|
||||||
|
MediaItem.Builder builder =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri(uri)
|
||||||
|
.setStreamKeys(downloadRequest != null ? downloadRequest.streamKeys : null)
|
||||||
|
.setCustomCacheKey(downloadRequest != null ? downloadRequest.customCacheKey : null)
|
||||||
|
.setMimeType(mimeType)
|
||||||
|
.setAdTagUri(intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix))
|
||||||
|
.setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix));
|
||||||
|
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;
|
||||||
|
String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
|
||||||
|
if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
String drmSchemeExtra =
|
||||||
|
intent.hasExtra(schemeKey)
|
||||||
|
? intent.getStringExtra(schemeKey)
|
||||||
|
: intent.getStringExtra(schemeUuidKey);
|
||||||
|
String[] drmSessionForClearTypesExtra =
|
||||||
|
intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix);
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
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_URL_EXTRA + extrasKeySuffix))
|
||||||
|
.setDrmSessionForClearTypes(toTrackTypeList(drmSessionForClearTypesExtra))
|
||||||
|
.setDrmMultiSession(
|
||||||
|
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false))
|
||||||
|
.setDrmLicenseRequestHeaders(headers);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Integer> toTrackTypeList(@Nullable String[] trackTypeStringsArray) {
|
||||||
|
if (trackTypeStringsArray == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
HashSet<Integer> trackTypes = new HashSet<>();
|
||||||
|
for (String trackTypeString : trackTypeStringsArray) {
|
||||||
|
switch (Util.toLowerInvariant(trackTypeString)) {
|
||||||
|
case "audio":
|
||||||
|
trackTypes.add(C.TRACK_TYPE_AUDIO);
|
||||||
|
break;
|
||||||
|
case "video":
|
||||||
|
trackTypes.add(C.TRACK_TYPE_VIDEO);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(trackTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addPlaybackPropertiesToIntent(
|
||||||
|
MediaItem.PlaybackProperties playbackProperties, Intent intent, String extrasKeySuffix) {
|
||||||
|
boolean isLive = false;
|
||||||
|
String sphericalStereoMode = null;
|
||||||
|
if (playbackProperties.tag instanceof Tag) {
|
||||||
|
Tag tag = (Tag) playbackProperties.tag;
|
||||||
|
isLive = tag.isLive;
|
||||||
|
sphericalStereoMode = tag.sphericalStereoMode;
|
||||||
|
}
|
||||||
|
intent
|
||||||
|
.putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType)
|
||||||
|
.putExtra(
|
||||||
|
AD_TAG_URI_EXTRA + extrasKeySuffix,
|
||||||
|
playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null)
|
||||||
|
.putExtra(IS_LIVE_EXTRA + extrasKeySuffix, isLive)
|
||||||
|
.putExtra(SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
|
||||||
|
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_URL_EXTRA + extrasKeySuffix,
|
||||||
|
checkNotNull(drmConfiguration.licenseUri).toString());
|
||||||
|
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmConfiguration.multiSession);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
ArrayList<String> typeStrings = new ArrayList<>();
|
||||||
|
for (int type : drmConfiguration.sessionForClearTypes) {
|
||||||
|
// Only audio and video are supported.
|
||||||
|
Assertions.checkState(type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO);
|
||||||
|
typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video");
|
||||||
|
}
|
||||||
|
intent.putExtra(
|
||||||
|
DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,37 +32,19 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.C.ContentType;
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
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.PlaybackPreparer;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.demo.Sample.UriSample;
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||||
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.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
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.BehindLiveWindowException;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
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.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
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.AdaptiveTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
|
|
@ -74,7 +56,7 @@ import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
|
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
import com.google.android.exoplayer2.util.ErrorMessageProvider;
|
||||||
import com.google.android.exoplayer2.util.EventLogger;
|
import com.google.android.exoplayer2.util.EventLogger;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
@ -82,49 +64,13 @@ import java.lang.reflect.Constructor;
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
import java.net.CookiePolicy;
|
import java.net.CookiePolicy;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||||
public class PlayerActivity extends AppCompatActivity
|
public class PlayerActivity extends AppCompatActivity
|
||||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
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";
|
|
||||||
|
|
||||||
// Saved instance state keys.
|
// Saved instance state keys.
|
||||||
|
|
||||||
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
|
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
|
||||||
|
|
@ -133,6 +79,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
private static final String KEY_AUTO_PLAY = "auto_play";
|
private static final String KEY_AUTO_PLAY = "auto_play";
|
||||||
|
|
||||||
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
DEFAULT_COOKIE_MANAGER = new CookieManager();
|
DEFAULT_COOKIE_MANAGER = new CookieManager();
|
||||||
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||||
|
|
@ -146,12 +93,11 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
private DataSource.Factory dataSourceFactory;
|
private DataSource.Factory dataSourceFactory;
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
private MediaSource mediaSource;
|
private List<MediaItem> mediaItems;
|
||||||
private DefaultTrackSelector trackSelector;
|
private DefaultTrackSelector trackSelector;
|
||||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||||
private DebugTextViewHelper debugViewHelper;
|
private DebugTextViewHelper debugViewHelper;
|
||||||
private TrackGroupArray lastSeenTrackGroupArray;
|
private TrackGroupArray lastSeenTrackGroupArray;
|
||||||
|
|
||||||
private boolean startAutoPlay;
|
private boolean startAutoPlay;
|
||||||
private int startWindow;
|
private int startWindow;
|
||||||
private long startPosition;
|
private long startPosition;
|
||||||
|
|
@ -166,7 +112,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
String sphericalStereoMode = intent.getStringExtra(IntentUtil.SPHERICAL_STEREO_MODE_EXTRA);
|
||||||
if (sphericalStereoMode != null) {
|
if (sphericalStereoMode != null) {
|
||||||
setTheme(R.style.PlayerTheme_Spherical);
|
setTheme(R.style.PlayerTheme_Spherical);
|
||||||
}
|
}
|
||||||
|
|
@ -188,11 +134,11 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
playerView.requestFocus();
|
playerView.requestFocus();
|
||||||
if (sphericalStereoMode != null) {
|
if (sphericalStereoMode != null) {
|
||||||
int stereoMode;
|
int stereoMode;
|
||||||
if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
|
if (IntentUtil.SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
|
||||||
stereoMode = C.STEREO_MODE_MONO;
|
stereoMode = C.STEREO_MODE_MONO;
|
||||||
} else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
|
} else if (IntentUtil.SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
|
||||||
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
|
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
|
||||||
} else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
|
} else if (IntentUtil.SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
|
||||||
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
|
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
|
||||||
} else {
|
} else {
|
||||||
showToast(R.string.error_unrecognized_stereo_mode);
|
showToast(R.string.error_unrecognized_stereo_mode);
|
||||||
|
|
@ -210,7 +156,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
} else {
|
} else {
|
||||||
DefaultTrackSelector.ParametersBuilder builder =
|
DefaultTrackSelector.ParametersBuilder builder =
|
||||||
new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
|
new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
|
||||||
boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false);
|
boolean tunneling = intent.getBooleanExtra(IntentUtil.TUNNELING_EXTRA, false);
|
||||||
if (Util.SDK_INT >= 21 && tunneling) {
|
if (Util.SDK_INT >= 21 && tunneling) {
|
||||||
builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this));
|
builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this));
|
||||||
}
|
}
|
||||||
|
|
@ -279,8 +225,9 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
public void onRequestPermissionsResult(
|
||||||
@NonNull int[] grantResults) {
|
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
if (grantResults.length == 0) {
|
if (grantResults.length == 0) {
|
||||||
// Empty results are triggered if a permission is requested while another request was already
|
// Empty results are triggered if a permission is requested while another request was already
|
||||||
// pending and can be safely ignored in this case.
|
// pending and can be safely ignored in this case.
|
||||||
|
|
@ -295,7 +242,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
updateTrackSelectorParameters();
|
updateTrackSelectorParameters();
|
||||||
updateStartPosition();
|
updateStartPosition();
|
||||||
|
|
@ -333,7 +280,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preparePlayback() {
|
public void preparePlayback() {
|
||||||
player.retry();
|
player.prepare();
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaybackControlView.VisibilityListener implementation
|
// PlaybackControlView.VisibilityListener implementation
|
||||||
|
|
@ -349,16 +296,16 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
|
|
||||||
mediaSource = createTopLevelMediaSource(intent);
|
mediaItems = createMediaItems(intent);
|
||||||
if (mediaSource == null) {
|
if (mediaItems.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSelection.Factory trackSelectionFactory;
|
TrackSelection.Factory trackSelectionFactory;
|
||||||
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
String abrAlgorithm = intent.getStringExtra(IntentUtil.ABR_ALGORITHM_EXTRA);
|
||||||
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
if (abrAlgorithm == null || IntentUtil.ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
||||||
trackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
trackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
||||||
} else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
|
} else if (IntentUtil.ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
|
||||||
trackSelectionFactory = new RandomTrackSelection.Factory();
|
trackSelectionFactory = new RandomTrackSelection.Factory();
|
||||||
} else {
|
} else {
|
||||||
showToast(R.string.error_unrecognized_abr_algorithm);
|
showToast(R.string.error_unrecognized_abr_algorithm);
|
||||||
|
|
@ -367,7 +314,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean preferExtensionDecoders =
|
boolean preferExtensionDecoders =
|
||||||
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||||
RenderersFactory renderersFactory =
|
RenderersFactory renderersFactory =
|
||||||
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
||||||
|
|
||||||
|
|
@ -377,174 +324,73 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
player =
|
player =
|
||||||
new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
|
new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
|
||||||
|
.setMediaSourceFactory(
|
||||||
|
new DefaultMediaSourceFactory(
|
||||||
|
/* context= */ this, dataSourceFactory, new AdSupportProvider()))
|
||||||
.setTrackSelector(trackSelector)
|
.setTrackSelector(trackSelector)
|
||||||
.build();
|
.build();
|
||||||
player.addListener(new PlayerEventListener());
|
player.addListener(new PlayerEventListener());
|
||||||
|
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
|
||||||
player.setPlayWhenReady(startAutoPlay);
|
player.setPlayWhenReady(startAutoPlay);
|
||||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||||
playerView.setPlayer(player);
|
playerView.setPlayer(player);
|
||||||
playerView.setPlaybackPreparer(this);
|
playerView.setPlaybackPreparer(this);
|
||||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||||
debugViewHelper.start();
|
debugViewHelper.start();
|
||||||
if (adsLoader != null) {
|
|
||||||
adsLoader.setPlayer(player);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
||||||
if (haveStartPosition) {
|
if (haveStartPosition) {
|
||||||
player.seekTo(startWindow, startPosition);
|
player.seekTo(startWindow, startPosition);
|
||||||
}
|
}
|
||||||
player.setMediaItem(mediaSource);
|
player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition);
|
||||||
player.prepare();
|
player.prepare();
|
||||||
updateButtonVisibility();
|
updateButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
private List<MediaItem> createMediaItems(Intent intent) {
|
||||||
private MediaSource createTopLevelMediaSource(Intent intent) {
|
|
||||||
String action = intent.getAction();
|
String action = intent.getAction();
|
||||||
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
|
boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action);
|
||||||
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
|
if (!actionIsListView && !IntentUtil.ACTION_VIEW.equals(action)) {
|
||||||
showToast(getString(R.string.unexpected_intent_action, action));
|
showToast(getString(R.string.unexpected_intent_action, action));
|
||||||
finish();
|
finish();
|
||||||
return null;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Sample intentAsSample = Sample.createFromIntent(intent);
|
List<MediaItem> mediaItems =
|
||||||
UriSample[] samples =
|
IntentUtil.createMediaItemsFromIntent(
|
||||||
intentAsSample instanceof Sample.PlaylistSample
|
intent, ((DemoApplication) getApplication()).getDownloadTracker());
|
||||||
? ((Sample.PlaylistSample) intentAsSample).children
|
boolean hasAds = false;
|
||||||
: new UriSample[] {(UriSample) intentAsSample};
|
for (int i = 0; i < mediaItems.size(); i++) {
|
||||||
|
MediaItem mediaItem = mediaItems.get(i);
|
||||||
|
|
||||||
boolean seenAdsTagUri = false;
|
if (!Util.checkCleartextTrafficPermitted(mediaItem)) {
|
||||||
for (UriSample sample : samples) {
|
|
||||||
seenAdsTagUri |= sample.adTagUri != null;
|
|
||||||
if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
|
|
||||||
showToast(R.string.error_cleartext_not_permitted);
|
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.
|
// The player will be reinitialized if the permission is granted.
|
||||||
return null;
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
MediaSource[] mediaSources = new MediaSource[samples.length];
|
MediaItem.DrmConfiguration drmConfiguration =
|
||||||
for (int i = 0; i < samples.length; i++) {
|
Assertions.checkNotNull(mediaItem.playbackProperties).drmConfiguration;
|
||||||
mediaSources[i] = createLeafMediaSource(samples[i]);
|
if (drmConfiguration != null) {
|
||||||
Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo;
|
if (Util.SDK_INT < 18) {
|
||||||
if (subtitleInfo != null) {
|
showToast(R.string.error_drm_unsupported_before_api_18);
|
||||||
Format subtitleFormat =
|
finish();
|
||||||
Format.createTextSampleFormat(
|
return Collections.emptyList();
|
||||||
/* id= */ null,
|
} else if (!MediaDrm.isCryptoSchemeSupported(drmConfiguration.uuid)) {
|
||||||
subtitleInfo.mimeType,
|
showToast(R.string.error_drm_unsupported_scheme);
|
||||||
C.SELECTION_FLAG_DEFAULT,
|
finish();
|
||||||
subtitleInfo.language);
|
return Collections.emptyList();
|
||||||
MediaSource subtitleMediaSource =
|
|
||||||
new SingleSampleMediaSource.Factory(dataSourceFactory)
|
|
||||||
.createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET);
|
|
||||||
mediaSources[i] = new MergingMediaSource(mediaSources[i], subtitleMediaSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
hasAds |= mediaItem.playbackProperties.adTagUri != null;
|
||||||
|
}
|
||||||
|
if (!hasAds) {
|
||||||
releaseAdsLoader();
|
releaseAdsLoader();
|
||||||
}
|
}
|
||||||
|
return mediaItems;
|
||||||
return mediaSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (!MediaDrm.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();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DownloadRequest downloadRequest =
|
|
||||||
((DemoApplication) getApplication())
|
|
||||||
.getDownloadTracker()
|
|
||||||
.getDownloadRequest(parameters.uri);
|
|
||||||
if (downloadRequest != null) {
|
|
||||||
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
|
|
||||||
}
|
|
||||||
return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MediaSource createLeafMediaSource(
|
|
||||||
Uri uri, String extension, DrmSessionManager<ExoMediaCrypto> 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() {
|
private void releasePlayer() {
|
||||||
|
|
@ -555,7 +401,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
debugViewHelper = null;
|
debugViewHelper = null;
|
||||||
player.release();
|
player.release();
|
||||||
player = null;
|
player = null;
|
||||||
mediaSource = null;
|
mediaItems = Collections.emptyList();
|
||||||
trackSelector = null;
|
trackSelector = null;
|
||||||
}
|
}
|
||||||
if (adsLoader != null) {
|
if (adsLoader != null) {
|
||||||
|
|
@ -597,37 +443,23 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
return ((DemoApplication) getApplication()).buildDataSourceFactory();
|
return ((DemoApplication) getApplication()).buildDataSourceFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns an ads media source, reusing the ads loader if one exists. */
|
/**
|
||||||
|
* Returns an ads loader for the Interactive Media Ads SDK if found in the classpath, or null
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
|
private AdsLoader maybeCreateAdsLoader(Uri adTagUri) {
|
||||||
// Load the extension source using reflection so the demo app doesn't have to depend on it.
|
// 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 {
|
try {
|
||||||
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
|
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
|
||||||
if (adsLoader == null) {
|
// Full class names used so the lint rule triggers should any of the classes move.
|
||||||
// Full class names used so the LINT.IfChange rule triggers should any of the classes move.
|
// LINT.IfChange
|
||||||
// LINT.IfChange
|
Constructor<? extends AdsLoader> loaderConstructor =
|
||||||
Constructor<? extends AdsLoader> loaderConstructor =
|
loaderClass
|
||||||
loaderClass
|
.asSubclass(AdsLoader.class)
|
||||||
.asSubclass(AdsLoader.class)
|
.getConstructor(android.content.Context.class, android.net.Uri.class);
|
||||||
.getConstructor(android.content.Context.class, android.net.Uri.class);
|
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
return loaderConstructor.newInstance(this, adTagUri);
|
||||||
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
|
||||||
}
|
|
||||||
MediaSourceFactory adMediaSourceFactory =
|
|
||||||
new MediaSourceFactory() {
|
|
||||||
@Override
|
|
||||||
public MediaSource createMediaSource(Uri uri) {
|
|
||||||
return PlayerActivity.this.createLeafMediaSource(
|
|
||||||
uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
@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) {
|
} catch (ClassNotFoundException e) {
|
||||||
// IMA extension not loaded.
|
// IMA extension not loaded.
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -672,7 +504,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
private class PlayerEventListener implements Player.EventListener {
|
private class PlayerEventListener implements Player.EventListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||||
if (playbackState == Player.STATE_ENDED) {
|
if (playbackState == Player.STATE_ENDED) {
|
||||||
showControls();
|
showControls();
|
||||||
}
|
}
|
||||||
|
|
@ -680,7 +512,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(ExoPlaybackException e) {
|
public void onPlayerError(@NonNull ExoPlaybackException e) {
|
||||||
if (isBehindLiveWindow(e)) {
|
if (isBehindLiveWindow(e)) {
|
||||||
clearStartPosition();
|
clearStartPosition();
|
||||||
initializePlayer();
|
initializePlayer();
|
||||||
|
|
@ -692,7 +524,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("ReferenceEquality")
|
@SuppressWarnings("ReferenceEquality")
|
||||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
public void onTracksChanged(
|
||||||
|
@NonNull TrackGroupArray trackGroups, @NonNull TrackSelectionArray trackSelections) {
|
||||||
updateButtonVisibility();
|
updateButtonVisibility();
|
||||||
if (trackGroups != lastSeenTrackGroupArray) {
|
if (trackGroups != lastSeenTrackGroupArray) {
|
||||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||||
|
|
@ -714,7 +547,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {
|
private class PlayerErrorMessageProvider implements ErrorMessageProvider<ExoPlaybackException> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pair<Integer, String> getErrorMessage(ExoPlaybackException e) {
|
@NonNull
|
||||||
|
public Pair<Integer, String> getErrorMessage(@NonNull ExoPlaybackException e) {
|
||||||
String errorString = getString(R.string.error_generic);
|
String errorString = getString(R.string.error_generic);
|
||||||
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
||||||
Exception cause = e.getRendererException();
|
Exception cause = e.getRendererException();
|
||||||
|
|
@ -744,4 +578,36 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
return Pair.create(0, errorString);
|
return Pair.create(0, errorString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class AdSupportProvider implements DefaultMediaSourceFactory.AdSupportProvider {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public AdsLoader getAdsLoader(Uri adTagUri) {
|
||||||
|
if (mediaItems.size() > 1) {
|
||||||
|
showToast(R.string.unsupported_ads_in_concatenation);
|
||||||
|
releaseAdsLoader();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||||
|
releaseAdsLoader();
|
||||||
|
loadedAdTagUri = adTagUri;
|
||||||
|
}
|
||||||
|
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||||
|
if (adsLoader == null) {
|
||||||
|
adsLoader = maybeCreateAdsLoader(adTagUri);
|
||||||
|
}
|
||||||
|
if (adsLoader != null) {
|
||||||
|
adsLoader.setPlayer(player);
|
||||||
|
} else {
|
||||||
|
showToast(R.string.ima_not_loaded);
|
||||||
|
}
|
||||||
|
return adsLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AdsLoader.AdViewProvider getAdViewProvider() {
|
||||||
|
return Assertions.checkNotNull(playerView);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,8 +15,13 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
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.content.res.AssetManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
|
@ -34,13 +39,14 @@ import android.widget.ExpandableListView.OnChildClickListener;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
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.ParserException;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
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.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||||
|
|
@ -55,33 +61,40 @@ import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/** An activity for selecting from a list of media samples. */
|
/** An activity for selecting from a list of media samples. */
|
||||||
public class SampleChooserActivity extends AppCompatActivity
|
public class SampleChooserActivity extends AppCompatActivity
|
||||||
implements DownloadTracker.Listener, OnChildClickListener {
|
implements DownloadTracker.Listener, OnChildClickListener {
|
||||||
|
|
||||||
private static final String TAG = "SampleChooserActivity";
|
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 boolean useExtensionRenderers;
|
||||||
private DownloadTracker downloadTracker;
|
private DownloadTracker downloadTracker;
|
||||||
private SampleAdapter sampleAdapter;
|
private SampleAdapter sampleAdapter;
|
||||||
private MenuItem preferExtensionDecodersMenuItem;
|
private MenuItem preferExtensionDecodersMenuItem;
|
||||||
private MenuItem randomAbrMenuItem;
|
private MenuItem randomAbrMenuItem;
|
||||||
private MenuItem tunnelingMenuItem;
|
private MenuItem tunnelingMenuItem;
|
||||||
|
private ExpandableListView sampleListView;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.sample_chooser_activity);
|
setContentView(R.layout.sample_chooser_activity);
|
||||||
sampleAdapter = new SampleAdapter();
|
sampleAdapter = new SampleAdapter();
|
||||||
ExpandableListView sampleListView = findViewById(R.id.sample_list);
|
sampleListView = findViewById(R.id.sample_list);
|
||||||
|
|
||||||
sampleListView.setAdapter(sampleAdapter);
|
sampleListView.setAdapter(sampleAdapter);
|
||||||
sampleListView.setOnChildClickListener(this);
|
sampleListView.setOnChildClickListener(this);
|
||||||
|
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
String dataUri = intent.getDataString();
|
String dataUri = intent.getDataString();
|
||||||
String[] uris;
|
|
||||||
if (dataUri != null) {
|
if (dataUri != null) {
|
||||||
uris = new String[] {dataUri};
|
uris = new String[] {dataUri};
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -105,8 +118,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
DemoApplication application = (DemoApplication) getApplication();
|
DemoApplication application = (DemoApplication) getApplication();
|
||||||
useExtensionRenderers = application.useExtensionRenderers();
|
useExtensionRenderers = application.useExtensionRenderers();
|
||||||
downloadTracker = application.getDownloadTracker();
|
downloadTracker = application.getDownloadTracker();
|
||||||
SampleListLoader loaderTask = new SampleListLoader();
|
loadSample();
|
||||||
loaderTask.execute(uris);
|
|
||||||
|
|
||||||
// Start the download service if it should be running but it's not currently.
|
// Start the download service if it should be running but it's not currently.
|
||||||
// Starting the service in the foreground causes notification flicker if there is no scheduled
|
// Starting the service in the foreground causes notification flicker if there is no scheduled
|
||||||
|
|
@ -157,67 +169,116 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
sampleAdapter.notifyDataSetChanged();
|
sampleAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
|
@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.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
loadSample();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSample() {
|
||||||
|
checkNotNull(uris);
|
||||||
|
|
||||||
|
for (int i = 0; i < uris.length; i++) {
|
||||||
|
Uri uri = Uri.parse(uris[i]);
|
||||||
|
if (Util.maybeRequestReadExternalStoragePermission(this, uri)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SampleListLoader loaderTask = new SampleListLoader();
|
||||||
|
loaderTask.execute(uris);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPlaylistGroups(final List<PlaylistGroup> groups, boolean sawError) {
|
||||||
if (sawError) {
|
if (sawError) {
|
||||||
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
sampleAdapter.setSampleGroups(groups);
|
sampleAdapter.setPlaylistGroups(groups);
|
||||||
|
|
||||||
|
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
|
||||||
|
|
||||||
|
int groupPosition = -1;
|
||||||
|
int childPosition = -1;
|
||||||
|
try {
|
||||||
|
groupPosition = preferences.getInt(GROUP_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||||
|
childPosition = preferences.getInt(CHILD_POSITION_PREFERENCE_KEY, /* defValue= */ -1);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
Log.w(TAG, "Saved position is not an int. Will not restore position.", e);
|
||||||
|
}
|
||||||
|
if (groupPosition != -1 && childPosition != -1) {
|
||||||
|
sampleListView.expandGroup(groupPosition); // shouldExpandGroup does not work without this.
|
||||||
|
sampleListView.setSelectedChild(groupPosition, childPosition, /* shouldExpandGroup= */ true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onChildClick(
|
public boolean onChildClick(
|
||||||
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
|
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 intent = new Intent(this, PlayerActivity.class);
|
||||||
intent.putExtra(
|
intent.putExtra(
|
||||||
PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
|
IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
|
||||||
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||||
String abrAlgorithm =
|
String abrAlgorithm =
|
||||||
isNonNullAndChecked(randomAbrMenuItem)
|
isNonNullAndChecked(randomAbrMenuItem)
|
||||||
? PlayerActivity.ABR_ALGORITHM_RANDOM
|
? IntentUtil.ABR_ALGORITHM_RANDOM
|
||||||
: PlayerActivity.ABR_ALGORITHM_DEFAULT;
|
: IntentUtil.ABR_ALGORITHM_DEFAULT;
|
||||||
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
intent.putExtra(IntentUtil.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||||
intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
|
intent.putExtra(IntentUtil.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
|
||||||
sample.addToIntent(intent);
|
IntentUtil.addToIntent(playlistHolder.mediaItems, intent);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSampleDownloadButtonClicked(Sample sample) {
|
private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) {
|
||||||
int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);
|
int downloadUnsupportedStringId = getDownloadUnsupportedStringId(playlistHolder);
|
||||||
if (downloadUnsupportedStringId != 0) {
|
if (downloadUnsupportedStringId != 0) {
|
||||||
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
|
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
|
||||||
.show();
|
.show();
|
||||||
} else {
|
} else {
|
||||||
UriSample uriSample = (UriSample) sample;
|
|
||||||
RenderersFactory renderersFactory =
|
RenderersFactory renderersFactory =
|
||||||
((DemoApplication) getApplication())
|
((DemoApplication) getApplication())
|
||||||
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||||
downloadTracker.toggleDownload(
|
downloadTracker.toggleDownload(
|
||||||
getSupportFragmentManager(),
|
getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory);
|
||||||
sample.name,
|
|
||||||
uriSample.uri,
|
|
||||||
uriSample.extension,
|
|
||||||
renderersFactory);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getDownloadUnsupportedStringId(Sample sample) {
|
private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) {
|
||||||
if (sample instanceof PlaylistSample) {
|
if (playlistHolder.mediaItems.size() > 1) {
|
||||||
return R.string.download_playlist_unsupported;
|
return R.string.download_playlist_unsupported;
|
||||||
}
|
}
|
||||||
UriSample uriSample = (UriSample) sample;
|
MediaItem.PlaybackProperties playbackProperties =
|
||||||
if (uriSample.drmInfo != null) {
|
checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties);
|
||||||
|
if (playbackProperties.drmConfiguration != null) {
|
||||||
return R.string.download_drm_unsupported;
|
return R.string.download_drm_unsupported;
|
||||||
}
|
}
|
||||||
if (uriSample.isLive) {
|
if (((IntentUtil.Tag) checkNotNull(playbackProperties.tag)).isLive) {
|
||||||
return R.string.download_live_unsupported;
|
return R.string.download_live_unsupported;
|
||||||
}
|
}
|
||||||
if (uriSample.adTagUri != null) {
|
if (playbackProperties.adTagUri != null) {
|
||||||
return R.string.download_ads_unsupported;
|
return R.string.download_ads_unsupported;
|
||||||
}
|
}
|
||||||
String scheme = uriSample.uri.getScheme();
|
String scheme = playbackProperties.uri.getScheme();
|
||||||
if (!("http".equals(scheme) || "https".equals(scheme))) {
|
if (!("http".equals(scheme) || "https".equals(scheme))) {
|
||||||
return R.string.download_scheme_unsupported;
|
return R.string.download_scheme_unsupported;
|
||||||
}
|
}
|
||||||
|
|
@ -229,13 +290,13 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
return menuItem != null && menuItem.isChecked();
|
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;
|
private boolean sawError;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<SampleGroup> doInBackground(String... uris) {
|
protected List<PlaylistGroup> doInBackground(String... uris) {
|
||||||
List<SampleGroup> result = new ArrayList<>();
|
List<PlaylistGroup> result = new ArrayList<>();
|
||||||
Context context = getApplicationContext();
|
Context context = getApplicationContext();
|
||||||
String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
|
String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
|
||||||
DataSource dataSource =
|
DataSource dataSource =
|
||||||
|
|
@ -244,7 +305,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
|
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
|
||||||
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
|
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
|
||||||
try {
|
try {
|
||||||
readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
|
readPlaylistGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error loading sample list: " + uri, e);
|
Log.e(TAG, "Error loading sample list: " + uri, e);
|
||||||
sawError = true;
|
sawError = true;
|
||||||
|
|
@ -256,21 +317,23 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(List<SampleGroup> result) {
|
protected void onPostExecute(List<PlaylistGroup> result) {
|
||||||
onSampleGroups(result, sawError);
|
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();
|
reader.beginArray();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
readSampleGroup(reader, groups);
|
readPlaylistGroup(reader, groups);
|
||||||
}
|
}
|
||||||
reader.endArray();
|
reader.endArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readSampleGroup(JsonReader reader, List<SampleGroup> groups) throws IOException {
|
private void readPlaylistGroup(JsonReader reader, List<PlaylistGroup> groups)
|
||||||
|
throws IOException {
|
||||||
String groupName = "";
|
String groupName = "";
|
||||||
ArrayList<Sample> samples = new ArrayList<>();
|
ArrayList<PlaylistHolder> playlistHolders = new ArrayList<>();
|
||||||
|
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
|
|
@ -282,7 +345,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
case "samples":
|
case "samples":
|
||||||
reader.beginArray();
|
reader.beginArray();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
samples.add(readEntry(reader, false));
|
playlistHolders.add(readEntry(reader, false));
|
||||||
}
|
}
|
||||||
reader.endArray();
|
reader.endArray();
|
||||||
break;
|
break;
|
||||||
|
|
@ -295,33 +358,28 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
|
|
||||||
SampleGroup group = getGroup(groupName, groups);
|
PlaylistGroup group = getGroup(groupName, groups);
|
||||||
group.samples.addAll(samples);
|
group.playlists.addAll(playlistHolders);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
|
||||||
String sampleName = null;
|
|
||||||
Uri uri = null;
|
Uri uri = null;
|
||||||
String extension = null;
|
String extension = null;
|
||||||
|
String title = null;
|
||||||
boolean isLive = false;
|
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;
|
String sphericalStereoMode = null;
|
||||||
List<Sample.SubtitleInfo> subtitleInfos = new ArrayList<>();
|
ArrayList<PlaylistHolder> children = null;
|
||||||
Uri subtitleUri = null;
|
Uri subtitleUri = null;
|
||||||
String subtitleMimeType = null;
|
String subtitleMimeType = null;
|
||||||
String subtitleLanguage = null;
|
String subtitleLanguage = null;
|
||||||
|
|
||||||
|
MediaItem.Builder mediaItem = new MediaItem.Builder();
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
String name = reader.nextName();
|
String name = reader.nextName();
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "name":
|
case "name":
|
||||||
sampleName = reader.nextString();
|
title = reader.nextString();
|
||||||
break;
|
break;
|
||||||
case "uri":
|
case "uri":
|
||||||
uri = Uri.parse(reader.nextString());
|
uri = Uri.parse(reader.nextString());
|
||||||
|
|
@ -330,38 +388,46 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
extension = reader.nextString();
|
extension = reader.nextString();
|
||||||
break;
|
break;
|
||||||
case "drm_scheme":
|
case "drm_scheme":
|
||||||
drmScheme = reader.nextString();
|
mediaItem.setDrmUuid(Util.getDrmUuid(reader.nextString()));
|
||||||
break;
|
break;
|
||||||
case "is_live":
|
case "is_live":
|
||||||
isLive = reader.nextBoolean();
|
isLive = reader.nextBoolean();
|
||||||
break;
|
break;
|
||||||
case "drm_license_url":
|
case "drm_license_url":
|
||||||
drmLicenseUrl = reader.nextString();
|
mediaItem.setDrmLicenseUri(reader.nextString());
|
||||||
break;
|
break;
|
||||||
case "drm_key_request_properties":
|
case "drm_key_request_properties":
|
||||||
ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
|
Map<String, String> requestHeaders = new HashMap<>();
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
drmKeyRequestPropertiesList.add(reader.nextName());
|
requestHeaders.put(reader.nextName(), reader.nextString());
|
||||||
drmKeyRequestPropertiesList.add(reader.nextString());
|
|
||||||
}
|
}
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
|
mediaItem.setDrmLicenseRequestHeaders(requestHeaders);
|
||||||
|
break;
|
||||||
|
case "drm_session_for_clear_types":
|
||||||
|
HashSet<Integer> drmSessionForClearTypes = new HashSet<>();
|
||||||
|
reader.beginArray();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
drmSessionForClearTypes.add(toTrackType(reader.nextString()));
|
||||||
|
}
|
||||||
|
reader.endArray();
|
||||||
|
mediaItem.setDrmSessionForClearTypes(new ArrayList<>(drmSessionForClearTypes));
|
||||||
break;
|
break;
|
||||||
case "drm_multi_session":
|
case "drm_multi_session":
|
||||||
drmMultiSession = reader.nextBoolean();
|
mediaItem.setDrmMultiSession(reader.nextBoolean());
|
||||||
break;
|
break;
|
||||||
case "playlist":
|
case "playlist":
|
||||||
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
|
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
|
||||||
playlistSamples = new ArrayList<>();
|
children = new ArrayList<>();
|
||||||
reader.beginArray();
|
reader.beginArray();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true));
|
children.add(readEntry(reader, /* insidePlaylist= */ true));
|
||||||
}
|
}
|
||||||
reader.endArray();
|
reader.endArray();
|
||||||
break;
|
break;
|
||||||
case "ad_tag_uri":
|
case "ad_tag_uri":
|
||||||
adTagUri = reader.nextString();
|
mediaItem.setAdTagUri(reader.nextString());
|
||||||
break;
|
break;
|
||||||
case "spherical_stereo_mode":
|
case "spherical_stereo_mode":
|
||||||
Assertions.checkState(
|
Assertions.checkState(
|
||||||
|
|
@ -382,67 +448,71 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
DrmInfo drmInfo =
|
|
||||||
drmScheme == null
|
if (children != null) {
|
||||||
? null
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
: new DrmInfo(
|
for (int i = 0; i < children.size(); i++) {
|
||||||
Util.getDrmUuid(drmScheme),
|
mediaItems.addAll(children.get(i).mediaItems);
|
||||||
drmLicenseUrl,
|
}
|
||||||
drmKeyRequestProperties,
|
return new PlaylistHolder(title, mediaItems);
|
||||||
drmMultiSession);
|
} else {
|
||||||
Sample.SubtitleInfo subtitleInfo =
|
mediaItem
|
||||||
subtitleUri == null
|
.setUri(uri)
|
||||||
? null
|
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
|
||||||
: new Sample.SubtitleInfo(
|
.setMimeType(IntentUtil.inferAdaptiveStreamMimeType(uri, extension))
|
||||||
|
.setTag(new IntentUtil.Tag(isLive, sphericalStereoMode));
|
||||||
|
if (subtitleUri != null) {
|
||||||
|
MediaItem.Subtitle subtitle =
|
||||||
|
new MediaItem.Subtitle(
|
||||||
subtitleUri,
|
subtitleUri,
|
||||||
Assertions.checkNotNull(
|
checkNotNull(
|
||||||
subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."),
|
subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."),
|
||||||
subtitleLanguage);
|
subtitleLanguage);
|
||||||
if (playlistSamples != null) {
|
mediaItem.setSubtitles(Collections.singletonList(subtitle));
|
||||||
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
|
}
|
||||||
return new PlaylistSample(sampleName, playlistSamplesArray);
|
return new PlaylistHolder(title, Collections.singletonList(mediaItem.build()));
|
||||||
} else {
|
|
||||||
return new UriSample(
|
|
||||||
sampleName,
|
|
||||||
uri,
|
|
||||||
extension,
|
|
||||||
isLive,
|
|
||||||
drmInfo,
|
|
||||||
adTagUri != null ? Uri.parse(adTagUri) : null,
|
|
||||||
sphericalStereoMode,
|
|
||||||
subtitleInfo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SampleGroup getGroup(String groupName, List<SampleGroup> groups) {
|
private PlaylistGroup getGroup(String groupName, List<PlaylistGroup> groups) {
|
||||||
for (int i = 0; i < groups.size(); i++) {
|
for (int i = 0; i < groups.size(); i++) {
|
||||||
if (Util.areEqual(groupName, groups.get(i).title)) {
|
if (Util.areEqual(groupName, groups.get(i).title)) {
|
||||||
return groups.get(i);
|
return groups.get(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SampleGroup group = new SampleGroup(groupName);
|
PlaylistGroup group = new PlaylistGroup(groupName);
|
||||||
groups.add(group);
|
groups.add(group);
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int toTrackType(String trackTypeString) {
|
||||||
|
switch (Util.toLowerInvariant(trackTypeString)) {
|
||||||
|
case "audio":
|
||||||
|
return C.TRACK_TYPE_AUDIO;
|
||||||
|
case "video":
|
||||||
|
return C.TRACK_TYPE_VIDEO;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
|
private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
|
||||||
|
|
||||||
private List<SampleGroup> sampleGroups;
|
private List<PlaylistGroup> playlistGroups;
|
||||||
|
|
||||||
public SampleAdapter() {
|
public SampleAdapter() {
|
||||||
sampleGroups = Collections.emptyList();
|
playlistGroups = Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSampleGroups(List<SampleGroup> sampleGroups) {
|
public void setPlaylistGroups(List<PlaylistGroup> playlistGroups) {
|
||||||
this.sampleGroups = sampleGroups;
|
this.playlistGroups = playlistGroups;
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Sample getChild(int groupPosition, int childPosition) {
|
public PlaylistHolder getChild(int groupPosition, int childPosition) {
|
||||||
return getGroup(groupPosition).samples.get(childPosition);
|
return getGroup(groupPosition).playlists.get(childPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -451,8 +521,12 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
|
public View getChildView(
|
||||||
View convertView, ViewGroup parent) {
|
int groupPosition,
|
||||||
|
int childPosition,
|
||||||
|
boolean isLastChild,
|
||||||
|
View convertView,
|
||||||
|
ViewGroup parent) {
|
||||||
View view = convertView;
|
View view = convertView;
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
|
view = getLayoutInflater().inflate(R.layout.sample_list_item, parent, false);
|
||||||
|
|
@ -466,12 +540,12 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getChildrenCount(int groupPosition) {
|
public int getChildrenCount(int groupPosition) {
|
||||||
return getGroup(groupPosition).samples.size();
|
return getGroup(groupPosition).playlists.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SampleGroup getGroup(int groupPosition) {
|
public PlaylistGroup getGroup(int groupPosition) {
|
||||||
return sampleGroups.get(groupPosition);
|
return playlistGroups.get(groupPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -480,8 +554,8 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
|
public View getGroupView(
|
||||||
ViewGroup parent) {
|
int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
|
||||||
View view = convertView;
|
View view = convertView;
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
view =
|
view =
|
||||||
|
|
@ -494,7 +568,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getGroupCount() {
|
public int getGroupCount() {
|
||||||
return sampleGroups.size();
|
return playlistGroups.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -509,18 +583,19 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
onSampleDownloadButtonClicked((Sample) view.getTag());
|
onSampleDownloadButtonClicked((PlaylistHolder) view.getTag());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeChildView(View view, Sample sample) {
|
private void initializeChildView(View view, PlaylistHolder playlistHolder) {
|
||||||
view.setTag(sample);
|
view.setTag(playlistHolder);
|
||||||
TextView sampleTitle = view.findViewById(R.id.sample_title);
|
TextView sampleTitle = view.findViewById(R.id.sample_title);
|
||||||
sampleTitle.setText(sample.name);
|
sampleTitle.setText(playlistHolder.title);
|
||||||
|
|
||||||
boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;
|
boolean canDownload = getDownloadUnsupportedStringId(playlistHolder) == 0;
|
||||||
boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);
|
boolean isDownloaded =
|
||||||
|
canDownload && downloadTracker.isDownloaded(playlistHolder.mediaItems.get(0));
|
||||||
ImageButton downloadButton = view.findViewById(R.id.download_button);
|
ImageButton downloadButton = view.findViewById(R.id.download_button);
|
||||||
downloadButton.setTag(sample);
|
downloadButton.setTag(playlistHolder);
|
||||||
downloadButton.setColorFilter(
|
downloadButton.setColorFilter(
|
||||||
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666);
|
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666);
|
||||||
downloadButton.setImageResource(
|
downloadButton.setImageResource(
|
||||||
|
|
@ -528,15 +603,26 @@ public class SampleChooserActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class SampleGroup {
|
private static final class PlaylistHolder {
|
||||||
|
|
||||||
public final String title;
|
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) {
|
||||||
|
Assertions.checkArgument(!mediaItems.isEmpty());
|
||||||
this.title = title;
|
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.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatDialog;
|
import androidx.appcompat.app.AppCompatDialog;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
@ -212,6 +213,7 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@NonNull
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
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
|
// 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
|
// 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
|
@Override
|
||||||
public void onDismiss(DialogInterface dialog) {
|
public void onDismiss(@NonNull DialogInterface dialog) {
|
||||||
super.onDismiss(dialog);
|
super.onDismiss(dialog);
|
||||||
onDismissListener.onDismiss(dialog);
|
onDismissListener.onDismiss(dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(
|
public View onCreateView(
|
||||||
LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
|
||||||
View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false);
|
View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false);
|
||||||
TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout);
|
TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout);
|
||||||
ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager);
|
ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager);
|
||||||
|
|
@ -290,6 +290,7 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@NonNull
|
||||||
public Fragment getItem(int position) {
|
public Fragment getItem(int position) {
|
||||||
return tabFragments.valueAt(position);
|
return tabFragments.valueAt(position);
|
||||||
}
|
}
|
||||||
|
|
@ -299,7 +300,6 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||||
return tabFragments.size();
|
return tabFragments.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
@Override
|
||||||
public CharSequence getPageTitle(int position) {
|
public CharSequence getPageTitle(int position) {
|
||||||
return getTrackTypeString(getResources(), tabTrackTypes.get(position));
|
return getTrackTypeString(getResources(), tabTrackTypes.get(position));
|
||||||
|
|
@ -341,7 +341,6 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||||
this.allowMultipleOverrides = allowMultipleOverrides;
|
this.allowMultipleOverrides = allowMultipleOverrides;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(
|
public View onCreateView(
|
||||||
LayoutInflater inflater,
|
LayoutInflater inflater,
|
||||||
|
|
@ -360,7 +359,8 @@ public final class TrackSelectionDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTrackSelectionChanged(boolean isDisabled, List<SelectionOverride> overrides) {
|
public void onTrackSelectionChanged(
|
||||||
|
boolean isDisabled, @NonNull List<SelectionOverride> overrides) {
|
||||||
this.isDisabled = isDisabled;
|
this.isDisabled = isDisabled;
|
||||||
this.overrides = overrides;
|
this.overrides = overrides;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
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.FrameworkMediaDrm;
|
||||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
|
@ -185,7 +184,7 @@ public final class MainActivity extends Activity {
|
||||||
? Assertions.checkNotNull(intent.getData())
|
? Assertions.checkNotNull(intent.getData())
|
||||||
: Uri.parse(DEFAULT_MEDIA_URI);
|
: Uri.parse(DEFAULT_MEDIA_URI);
|
||||||
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
|
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
|
||||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
DrmSessionManager drmSessionManager;
|
||||||
if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
|
if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
|
||||||
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
|
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
|
||||||
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
|
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
|
||||||
|
|
@ -220,8 +219,9 @@ public final class MainActivity extends Activity {
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
|
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
|
||||||
player.prepare(mediaSource);
|
player.setMediaSource(mediaSource);
|
||||||
player.setPlayWhenReady(true);
|
player.prepare();
|
||||||
|
player.play();
|
||||||
player.setRepeatMode(Player.REPEAT_MODE_ALL);
|
player.setRepeatMode(Player.REPEAT_MODE_ALL);
|
||||||
|
|
||||||
surfaceControl =
|
surfaceControl =
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,14 @@ a custom track selector the choice of `Renderer` is up to your implementation.
|
||||||
You need to make sure you are passing a `Libgav1VideoRenderer` to the player and
|
You need to make sure you are passing a `Libgav1VideoRenderer` to the player and
|
||||||
then you need to implement your own logic to use the renderer for a given track.
|
then you need to implement your own logic to use the renderer for a given track.
|
||||||
|
|
||||||
|
## Using the extension in the demo application ##
|
||||||
|
|
||||||
|
To try out playback using the extension in the [demo application][], see
|
||||||
|
[enabling extension decoders][].
|
||||||
|
|
||||||
|
[demo application]: https://exoplayer.dev/demo-application.html
|
||||||
|
[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
|
||||||
|
|
||||||
## Rendering options ##
|
## Rendering options ##
|
||||||
|
|
||||||
There are two possibilities for rendering the output `Libgav1VideoRenderer`
|
There are two possibilities for rendering the output `Libgav1VideoRenderer`
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ if (project.file('src/main/jni/libgav1').exists()) {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.av1;
|
package com.google.android.exoplayer2.ext.av1;
|
||||||
|
|
||||||
|
import static java.lang.Runtime.getRuntime;
|
||||||
|
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
|
@ -44,7 +46,9 @@ import java.nio.ByteBuffer;
|
||||||
* @param numInputBuffers Number of input buffers.
|
* @param numInputBuffers Number of input buffers.
|
||||||
* @param numOutputBuffers Number of output buffers.
|
* @param numOutputBuffers Number of output buffers.
|
||||||
* @param initialInputBufferSize The initial size of each input buffer, in bytes.
|
* @param initialInputBufferSize The initial size of each input buffer, in bytes.
|
||||||
* @param threads Number of threads libgav1 will use to decode.
|
* @param threads Number of threads libgav1 will use to decode. If {@link
|
||||||
|
* Libgav1VideoRenderer#THREAD_COUNT_AUTODETECT} is passed, then this class will auto detect
|
||||||
|
* the number of threads to be used.
|
||||||
* @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder.
|
* @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder.
|
||||||
*/
|
*/
|
||||||
public Gav1Decoder(
|
public Gav1Decoder(
|
||||||
|
|
@ -56,6 +60,16 @@ import java.nio.ByteBuffer;
|
||||||
if (!Gav1Library.isAvailable()) {
|
if (!Gav1Library.isAvailable()) {
|
||||||
throw new Gav1DecoderException("Failed to load decoder native library.");
|
throw new Gav1DecoderException("Failed to load decoder native library.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (threads == Libgav1VideoRenderer.THREAD_COUNT_AUTODETECT) {
|
||||||
|
// Try to get the optimal number of threads from the AV1 heuristic.
|
||||||
|
threads = gav1GetThreads();
|
||||||
|
if (threads <= 0) {
|
||||||
|
// If that is not available, default to the number of available processors.
|
||||||
|
threads = getRuntime().availableProcessors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gav1DecoderContext = gav1Init(threads);
|
gav1DecoderContext = gav1Init(threads);
|
||||||
if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) {
|
if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) {
|
||||||
throw new Gav1DecoderException(
|
throw new Gav1DecoderException(
|
||||||
|
|
@ -88,8 +102,8 @@ import java.nio.ByteBuffer;
|
||||||
return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
|
return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
protected Gav1DecoderException decode(
|
protected Gav1DecoderException decode(
|
||||||
VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
|
VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
|
||||||
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
|
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
|
||||||
|
|
@ -203,7 +217,7 @@ import java.nio.ByteBuffer;
|
||||||
* @param context Decoder context.
|
* @param context Decoder context.
|
||||||
* @param surface Output surface.
|
* @param surface Output surface.
|
||||||
* @param outputBuffer Output buffer with the decoded frame.
|
* @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(
|
private native int gav1RenderFrame(
|
||||||
long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
|
long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
|
||||||
|
|
@ -225,10 +239,17 @@ import java.nio.ByteBuffer;
|
||||||
private native String gav1GetErrorMessage(long context);
|
private native String gav1GetErrorMessage(long context);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether an error occured.
|
* Returns whether an error occurred.
|
||||||
*
|
*
|
||||||
* @param context Decoder context.
|
* @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);
|
private native int gav1CheckError(long context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the optimal number of threads to be used for AV1 decoding.
|
||||||
|
*
|
||||||
|
* @return Optimal number of threads if there was no error, 0 if an error occurred.
|
||||||
|
*/
|
||||||
|
private native int gav1GetThreads();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.av1;
|
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. */
|
/** Thrown when a libgav1 decoder error occurs. */
|
||||||
public final class Gav1DecoderException extends VideoDecoderException {
|
public final class Gav1DecoderException extends DecoderException {
|
||||||
|
|
||||||
/* package */ Gav1DecoderException(String message) {
|
/* package */ Gav1DecoderException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|
|
||||||
|
|
@ -15,45 +15,31 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.av1;
|
package com.google.android.exoplayer2.ext.av1;
|
||||||
|
|
||||||
import static java.lang.Runtime.getRuntime;
|
|
||||||
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
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.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.drm.ExoMediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.TraceUtil;
|
import com.google.android.exoplayer2.util.TraceUtil;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;
|
import com.google.android.exoplayer2.video.DecoderVideoRenderer;
|
||||||
import com.google.android.exoplayer2.video.VideoDecoderException;
|
|
||||||
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
|
|
||||||
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
|
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
|
||||||
import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
|
|
||||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||||
|
|
||||||
/**
|
/** Decodes and renders video using libgav1 decoder. */
|
||||||
* Decodes and renders video using libgav1 decoder.
|
public class Libgav1VideoRenderer extends DecoderVideoRenderer {
|
||||||
*
|
|
||||||
* <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 {
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to use as many threads as performance processors available on the device. If the
|
||||||
|
* number of performance processors cannot be detected, the number of available processors is
|
||||||
|
* used.
|
||||||
|
*/
|
||||||
|
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_INPUT_BUFFERS = 4;
|
||||||
private static final int DEFAULT_NUM_OF_OUTPUT_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. */
|
/* Default size based on 720p resolution video compressed by a factor of two. */
|
||||||
|
|
@ -73,7 +59,7 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||||
@Nullable private Gav1Decoder decoder;
|
@Nullable private Gav1Decoder decoder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Libgav1VideoRenderer.
|
* Creates a new instance.
|
||||||
*
|
*
|
||||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||||
* can attempt to seamlessly join an ongoing playback.
|
* can attempt to seamlessly join an ongoing playback.
|
||||||
|
|
@ -93,13 +79,13 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||||
eventHandler,
|
eventHandler,
|
||||||
eventListener,
|
eventListener,
|
||||||
maxDroppedFramesToNotify,
|
maxDroppedFramesToNotify,
|
||||||
/* threads= */ getRuntime().availableProcessors(),
|
THREAD_COUNT_AUTODETECT,
|
||||||
DEFAULT_NUM_OF_INPUT_BUFFERS,
|
DEFAULT_NUM_OF_INPUT_BUFFERS,
|
||||||
DEFAULT_NUM_OF_OUTPUT_BUFFERS);
|
DEFAULT_NUM_OF_OUTPUT_BUFFERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Libgav1VideoRenderer.
|
* Creates a new instance.
|
||||||
*
|
*
|
||||||
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||||
* can attempt to seamlessly join an ongoing playback.
|
* can attempt to seamlessly join an ongoing playback.
|
||||||
|
|
@ -108,7 +94,9 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
||||||
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||||
* @param threads Number of threads libgav1 will use to decode.
|
* @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 numInputBuffers Number of input buffers.
|
||||||
* @param numOutputBuffers Number of output buffers.
|
* @param numOutputBuffers Number of output buffers.
|
||||||
*/
|
*/
|
||||||
|
|
@ -120,38 +108,33 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||||
int threads,
|
int threads,
|
||||||
int numInputBuffers,
|
int numInputBuffers,
|
||||||
int numOutputBuffers) {
|
int numOutputBuffers) {
|
||||||
super(
|
super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify);
|
||||||
allowedJoiningTimeMs,
|
|
||||||
eventHandler,
|
|
||||||
eventListener,
|
|
||||||
maxDroppedFramesToNotify,
|
|
||||||
/* drmSessionManager= */ null,
|
|
||||||
/* playClearSamplesWithoutKeys= */ false);
|
|
||||||
this.threads = threads;
|
this.threads = threads;
|
||||||
this.numInputBuffers = numInputBuffers;
|
this.numInputBuffers = numInputBuffers;
|
||||||
this.numOutputBuffers = numOutputBuffers;
|
this.numOutputBuffers = numOutputBuffers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int supportsFormatInternal(
|
public String getName() {
|
||||||
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
|
return TAG;
|
||||||
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|
|
||||||
|| !Gav1Library.isAvailable()) {
|
|
||||||
return FORMAT_UNSUPPORTED_TYPE;
|
|
||||||
}
|
|
||||||
if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
|
||||||
return FORMAT_UNSUPPORTED_DRM;
|
|
||||||
}
|
|
||||||
return FORMAT_HANDLED | ADAPTIVE_SEAMLESS;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected SimpleDecoder<
|
@Capabilities
|
||||||
VideoDecoderInputBuffer,
|
public final int supportsFormat(Format format) {
|
||||||
? extends VideoDecoderOutputBuffer,
|
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|
||||||
? extends VideoDecoderException>
|
|| !Gav1Library.isAvailable()) {
|
||||||
createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
|
||||||
throws VideoDecoderException {
|
}
|
||||||
|
if (format.drmInitData != null && format.exoMediaCryptoType == null) {
|
||||||
|
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
|
||||||
|
}
|
||||||
|
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Gav1Decoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||||
|
throws Gav1DecoderException {
|
||||||
TraceUtil.beginSection("createGav1Decoder");
|
TraceUtil.beginSection("createGav1Decoder");
|
||||||
int initialInputBufferSize =
|
int initialInputBufferSize =
|
||||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||||
|
|
@ -180,16 +163,8 @@ public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayerMessage.Target implementation.
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
|
protected boolean canKeepCodec(Format oldFormat, Format newFormat) {
|
||||||
if (messageType == C.MSG_SET_SURFACE) {
|
return true;
|
||||||
setOutputSurface((Surface) message);
|
|
||||||
} else if (messageType == C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) {
|
|
||||||
setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message);
|
|
||||||
} else {
|
|
||||||
super.handleMessage(messageType, message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,15 @@ project(libgav1JNI C CXX)
|
||||||
# armeabi-v7a build. This flag enables it.
|
# armeabi-v7a build. This flag enables it.
|
||||||
if(${ANDROID_ABI} MATCHES "armeabi-v7a")
|
if(${ANDROID_ABI} MATCHES "armeabi-v7a")
|
||||||
add_compile_options("-mfpu=neon")
|
add_compile_options("-mfpu=neon")
|
||||||
|
add_compile_options("-marm")
|
||||||
add_compile_options("-fPIC")
|
add_compile_options("-fPIC")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
string(TOLOWER "${CMAKE_BUILD_TYPE}" build_type)
|
||||||
|
if(build_type MATCHES "^rel")
|
||||||
|
add_compile_options("-O2")
|
||||||
|
endif()
|
||||||
|
|
||||||
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
|
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
|
set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
|
||||||
set(libgav1_jni_output_directory
|
set(libgav1_jni_output_directory
|
||||||
|
|
@ -38,7 +44,9 @@ add_subdirectory("${libgav1_root}"
|
||||||
# Build libgav1JNI.
|
# Build libgav1JNI.
|
||||||
add_library(gav1JNI
|
add_library(gav1JNI
|
||||||
SHARED
|
SHARED
|
||||||
gav1_jni.cc)
|
gav1_jni.cc
|
||||||
|
cpu_info.cc
|
||||||
|
cpu_info.h)
|
||||||
|
|
||||||
# Locate NDK log library.
|
# Locate NDK log library.
|
||||||
find_library(android_log_lib log)
|
find_library(android_log_lib log)
|
||||||
|
|
|
||||||
153
extensions/av1/src/main/jni/cpu_info.cc
Normal file
153
extensions/av1/src/main/jni/cpu_info.cc
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
#include "cpu_info.h" // NOLINT
|
||||||
|
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <climits>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace gav1_jni {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Note: The code in this file needs to use the 'long' type because it is the
|
||||||
|
// return type of the Standard C Library function strtol(). The linter warnings
|
||||||
|
// are suppressed with NOLINT comments since they are integers at runtime.
|
||||||
|
|
||||||
|
// Returns the number of online processor cores.
|
||||||
|
int GetNumberOfProcessorsOnline() {
|
||||||
|
// See https://developer.android.com/ndk/guides/cpu-features.
|
||||||
|
long num_cpus = sysconf(_SC_NPROCESSORS_ONLN); // NOLINT
|
||||||
|
if (num_cpus < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// It is safe to cast num_cpus to int. sysconf(_SC_NPROCESSORS_ONLN) returns
|
||||||
|
// the return value of get_nprocs(), which is an int.
|
||||||
|
return static_cast<int>(num_cpus);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// These CPUs support heterogeneous multiprocessing.
|
||||||
|
#if defined(__arm__) || defined(__aarch64__)
|
||||||
|
|
||||||
|
// A helper function used by GetNumberOfPerformanceCoresOnline().
|
||||||
|
//
|
||||||
|
// Returns the cpuinfo_max_freq value (in kHz) of the given CPU. Returns 0 on
|
||||||
|
// failure.
|
||||||
|
long GetCpuinfoMaxFreq(int cpu_index) { // NOLINT
|
||||||
|
char buffer[128];
|
||||||
|
const int rv = snprintf(
|
||||||
|
buffer, sizeof(buffer),
|
||||||
|
"/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", cpu_index);
|
||||||
|
if (rv < 0 || rv >= sizeof(buffer)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
FILE* file = fopen(buffer, "r");
|
||||||
|
if (file == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char* const str = fgets(buffer, sizeof(buffer), file);
|
||||||
|
fclose(file);
|
||||||
|
if (str == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const long freq = strtol(str, nullptr, 10); // NOLINT
|
||||||
|
if (freq <= 0 || freq == LONG_MAX) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return freq;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the number of performance CPU cores that are online. The number of
|
||||||
|
// efficiency CPU cores is subtracted from the total number of CPU cores. Uses
|
||||||
|
// cpuinfo_max_freq to determine whether a CPU is a performance core or an
|
||||||
|
// efficiency core.
|
||||||
|
//
|
||||||
|
// This function is not perfect. For example, the Snapdragon 632 SoC used in
|
||||||
|
// Motorola Moto G7 has performance and efficiency cores with the same
|
||||||
|
// cpuinfo_max_freq but different cpuinfo_min_freq. This function fails to
|
||||||
|
// differentiate the two kinds of cores and reports all the cores as
|
||||||
|
// performance cores.
|
||||||
|
int GetNumberOfPerformanceCoresOnline() {
|
||||||
|
// Get the online CPU list. Some examples of the online CPU list are:
|
||||||
|
// "0-7"
|
||||||
|
// "0"
|
||||||
|
// "0-1,2,3,4-7"
|
||||||
|
FILE* file = fopen("/sys/devices/system/cpu/online", "r");
|
||||||
|
if (file == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char online[512];
|
||||||
|
char* const str = fgets(online, sizeof(online), file);
|
||||||
|
fclose(file);
|
||||||
|
file = nullptr;
|
||||||
|
if (str == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count the number of the slowest CPUs. Some SoCs such as Snapdragon 855
|
||||||
|
// have performance cores with different max frequencies, so only the slowest
|
||||||
|
// CPUs are efficiency cores. If we count the number of the fastest CPUs, we
|
||||||
|
// will fail to count the second fastest performance cores.
|
||||||
|
long slowest_cpu_freq = LONG_MAX; // NOLINT
|
||||||
|
int num_slowest_cpus = 0;
|
||||||
|
int num_cpus = 0;
|
||||||
|
const char* cp = online;
|
||||||
|
int range_begin = -1;
|
||||||
|
while (true) {
|
||||||
|
char* str_end;
|
||||||
|
const int cpu = static_cast<int>(strtol(cp, &str_end, 10)); // NOLINT
|
||||||
|
if (str_end == cp) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cp = str_end;
|
||||||
|
if (*cp == '-') {
|
||||||
|
range_begin = cpu;
|
||||||
|
} else {
|
||||||
|
if (range_begin == -1) {
|
||||||
|
range_begin = cpu;
|
||||||
|
}
|
||||||
|
|
||||||
|
num_cpus += cpu - range_begin + 1;
|
||||||
|
for (int i = range_begin; i <= cpu; ++i) {
|
||||||
|
const long freq = GetCpuinfoMaxFreq(i); // NOLINT
|
||||||
|
if (freq <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (freq < slowest_cpu_freq) {
|
||||||
|
slowest_cpu_freq = freq;
|
||||||
|
num_slowest_cpus = 0;
|
||||||
|
}
|
||||||
|
if (freq == slowest_cpu_freq) {
|
||||||
|
++num_slowest_cpus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
range_begin = -1;
|
||||||
|
}
|
||||||
|
if (*cp == '\0') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are faster CPU cores than the slowest CPU cores, exclude the
|
||||||
|
// slowest CPU cores.
|
||||||
|
if (num_slowest_cpus < num_cpus) {
|
||||||
|
num_cpus -= num_slowest_cpus;
|
||||||
|
}
|
||||||
|
return num_cpus;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
// Assume symmetric multiprocessing.
|
||||||
|
int GetNumberOfPerformanceCoresOnline() {
|
||||||
|
return GetNumberOfProcessorsOnline();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace gav1_jni
|
||||||
13
extensions/av1/src/main/jni/cpu_info.h
Normal file
13
extensions/av1/src/main/jni/cpu_info.h
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#ifndef EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_
|
||||||
|
#define EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_
|
||||||
|
|
||||||
|
namespace gav1_jni {
|
||||||
|
|
||||||
|
// Returns the number of performance cores that are available for AV1 decoding.
|
||||||
|
// This is a heuristic that works on most common android devices. Returns 0 on
|
||||||
|
// error or if the number of performance cores cannot be determined.
|
||||||
|
int GetNumberOfPerformanceCoresOnline();
|
||||||
|
|
||||||
|
} // namespace gav1_jni
|
||||||
|
|
||||||
|
#endif // EXOPLAYER_V2_EXTENSIONS_AV1_SRC_MAIN_JNI_CPU_INFO_H_
|
||||||
|
|
@ -27,10 +27,12 @@
|
||||||
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <mutex> // NOLINT
|
#include <mutex> // NOLINT
|
||||||
#include <new>
|
#include <new>
|
||||||
|
|
||||||
|
#include "cpu_info.h" // NOLINT
|
||||||
#include "gav1/decoder.h"
|
#include "gav1/decoder.h"
|
||||||
|
|
||||||
#define LOG_TAG "gav1_jni"
|
#define LOG_TAG "gav1_jni"
|
||||||
|
|
@ -71,7 +73,7 @@ const int kImageFormatYV12 = 0x32315659;
|
||||||
// Output modes.
|
// Output modes.
|
||||||
const int kOutputModeYuv = 0;
|
const int kOutputModeYuv = 0;
|
||||||
const int kOutputModeSurfaceYuv = 1;
|
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
|
// LINT.IfChange
|
||||||
const int kColorSpaceUnknown = 0;
|
const int kColorSpaceUnknown = 0;
|
||||||
|
|
@ -121,18 +123,22 @@ const char* GetJniErrorMessage(JniStatusCode error_code) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manages Libgav1FrameBuffer and reference information.
|
// Manages frame buffer and reference information.
|
||||||
class JniFrameBuffer {
|
class JniFrameBuffer {
|
||||||
public:
|
public:
|
||||||
explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) {
|
explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) {}
|
||||||
gav1_frame_buffer_.private_data = &id_;
|
|
||||||
}
|
|
||||||
~JniFrameBuffer() {
|
~JniFrameBuffer() {
|
||||||
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
||||||
delete[] gav1_frame_buffer_.data[plane_index];
|
delete[] raw_buffer_[plane_index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not copyable or movable.
|
||||||
|
JniFrameBuffer(const JniFrameBuffer&) = delete;
|
||||||
|
JniFrameBuffer(JniFrameBuffer&&) = delete;
|
||||||
|
JniFrameBuffer& operator=(const JniFrameBuffer&) = delete;
|
||||||
|
JniFrameBuffer& operator=(JniFrameBuffer&&) = delete;
|
||||||
|
|
||||||
void SetFrameData(const libgav1::DecoderBuffer& decoder_buffer) {
|
void SetFrameData(const libgav1::DecoderBuffer& decoder_buffer) {
|
||||||
for (int plane_index = kPlaneY; plane_index < decoder_buffer.NumPlanes();
|
for (int plane_index = kPlaneY; plane_index < decoder_buffer.NumPlanes();
|
||||||
plane_index++) {
|
plane_index++) {
|
||||||
|
|
@ -160,9 +166,8 @@ class JniFrameBuffer {
|
||||||
void RemoveReference() { reference_count_--; }
|
void RemoveReference() { reference_count_--; }
|
||||||
bool InUse() const { return reference_count_ != 0; }
|
bool InUse() const { return reference_count_ != 0; }
|
||||||
|
|
||||||
const Libgav1FrameBuffer& GetGav1FrameBuffer() const {
|
uint8_t* RawBuffer(int plane_index) const { return raw_buffer_[plane_index]; }
|
||||||
return gav1_frame_buffer_;
|
void* BufferPrivateData() const { return const_cast<int*>(&id_); }
|
||||||
}
|
|
||||||
|
|
||||||
// Attempts to reallocate data planes if the existing ones don't have enough
|
// Attempts to reallocate data planes if the existing ones don't have enough
|
||||||
// capacity. Returns true if the allocation was successful or wasn't needed,
|
// capacity. Returns true if the allocation was successful or wasn't needed,
|
||||||
|
|
@ -172,15 +177,14 @@ class JniFrameBuffer {
|
||||||
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
|
||||||
const int min_size =
|
const int min_size =
|
||||||
(plane_index == kPlaneY) ? y_plane_min_size : uv_plane_min_size;
|
(plane_index == kPlaneY) ? y_plane_min_size : uv_plane_min_size;
|
||||||
if (gav1_frame_buffer_.size[plane_index] >= min_size) continue;
|
if (raw_buffer_size_[plane_index] >= min_size) continue;
|
||||||
delete[] gav1_frame_buffer_.data[plane_index];
|
delete[] raw_buffer_[plane_index];
|
||||||
gav1_frame_buffer_.data[plane_index] =
|
raw_buffer_[plane_index] = new (std::nothrow) uint8_t[min_size];
|
||||||
new (std::nothrow) uint8_t[min_size];
|
if (!raw_buffer_[plane_index]) {
|
||||||
if (!gav1_frame_buffer_.data[plane_index]) {
|
raw_buffer_size_[plane_index] = 0;
|
||||||
gav1_frame_buffer_.size[plane_index] = 0;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
gav1_frame_buffer_.size[plane_index] = min_size;
|
raw_buffer_size_[plane_index] = min_size;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -190,9 +194,12 @@ class JniFrameBuffer {
|
||||||
uint8_t* plane_[kMaxPlanes];
|
uint8_t* plane_[kMaxPlanes];
|
||||||
int displayed_width_[kMaxPlanes];
|
int displayed_width_[kMaxPlanes];
|
||||||
int displayed_height_[kMaxPlanes];
|
int displayed_height_[kMaxPlanes];
|
||||||
int id_;
|
const int id_;
|
||||||
int reference_count_;
|
int reference_count_;
|
||||||
Libgav1FrameBuffer gav1_frame_buffer_ = {};
|
// Pointers to the raw buffers allocated for the data planes.
|
||||||
|
uint8_t* raw_buffer_[kMaxPlanes] = {};
|
||||||
|
// Sizes of the raw buffers in bytes.
|
||||||
|
size_t raw_buffer_size_[kMaxPlanes] = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Manages frame buffers used by libgav1 decoder and ExoPlayer.
|
// Manages frame buffers used by libgav1 decoder and ExoPlayer.
|
||||||
|
|
@ -210,7 +217,7 @@ class JniBufferManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
JniStatusCode GetBuffer(size_t y_plane_min_size, size_t uv_plane_min_size,
|
JniStatusCode GetBuffer(size_t y_plane_min_size, size_t uv_plane_min_size,
|
||||||
Libgav1FrameBuffer* frame_buffer) {
|
JniFrameBuffer** jni_buffer) {
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
JniFrameBuffer* output_buffer;
|
JniFrameBuffer* output_buffer;
|
||||||
|
|
@ -230,7 +237,7 @@ class JniBufferManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
output_buffer->AddReference();
|
output_buffer->AddReference();
|
||||||
*frame_buffer = output_buffer->GetGav1FrameBuffer();
|
*jni_buffer = output_buffer;
|
||||||
|
|
||||||
return kJniStatusOk;
|
return kJniStatusOk;
|
||||||
}
|
}
|
||||||
|
|
@ -316,29 +323,46 @@ struct JniContext {
|
||||||
JniStatusCode jni_status_code = kJniStatusOk;
|
JniStatusCode jni_status_code = kJniStatusOk;
|
||||||
};
|
};
|
||||||
|
|
||||||
int Libgav1GetFrameBuffer(void* private_data, size_t y_plane_min_size,
|
Libgav1StatusCode Libgav1GetFrameBuffer(void* callback_private_data,
|
||||||
size_t uv_plane_min_size,
|
int bitdepth,
|
||||||
Libgav1FrameBuffer* frame_buffer) {
|
libgav1::ImageFormat image_format,
|
||||||
JniContext* const context = reinterpret_cast<JniContext*>(private_data);
|
int width, int height, int left_border,
|
||||||
|
int right_border, int top_border,
|
||||||
|
int bottom_border, int stride_alignment,
|
||||||
|
libgav1::FrameBuffer* frame_buffer) {
|
||||||
|
libgav1::FrameBufferInfo info;
|
||||||
|
Libgav1StatusCode status = libgav1::ComputeFrameBufferInfo(
|
||||||
|
bitdepth, image_format, width, height, left_border, right_border,
|
||||||
|
top_border, bottom_border, stride_alignment, &info);
|
||||||
|
if (status != kLibgav1StatusOk) return status;
|
||||||
|
|
||||||
|
JniContext* const context = static_cast<JniContext*>(callback_private_data);
|
||||||
|
JniFrameBuffer* jni_buffer;
|
||||||
context->jni_status_code = context->buffer_manager.GetBuffer(
|
context->jni_status_code = context->buffer_manager.GetBuffer(
|
||||||
y_plane_min_size, uv_plane_min_size, frame_buffer);
|
info.y_buffer_size, info.uv_buffer_size, &jni_buffer);
|
||||||
if (context->jni_status_code != kJniStatusOk) {
|
if (context->jni_status_code != kJniStatusOk) {
|
||||||
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
||||||
return -1;
|
return kLibgav1StatusOutOfMemory;
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
|
uint8_t* const y_buffer = jni_buffer->RawBuffer(0);
|
||||||
|
uint8_t* const u_buffer =
|
||||||
|
(info.uv_buffer_size != 0) ? jni_buffer->RawBuffer(1) : nullptr;
|
||||||
|
uint8_t* const v_buffer =
|
||||||
|
(info.uv_buffer_size != 0) ? jni_buffer->RawBuffer(2) : nullptr;
|
||||||
|
|
||||||
|
return libgav1::SetFrameBuffer(&info, y_buffer, u_buffer, v_buffer,
|
||||||
|
jni_buffer->BufferPrivateData(), frame_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Libgav1ReleaseFrameBuffer(void* private_data,
|
void Libgav1ReleaseFrameBuffer(void* callback_private_data,
|
||||||
Libgav1FrameBuffer* frame_buffer) {
|
void* buffer_private_data) {
|
||||||
JniContext* const context = reinterpret_cast<JniContext*>(private_data);
|
JniContext* const context = static_cast<JniContext*>(callback_private_data);
|
||||||
const int buffer_id = *reinterpret_cast<int*>(frame_buffer->private_data);
|
const int buffer_id = *static_cast<const int*>(buffer_private_data);
|
||||||
context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id);
|
context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id);
|
||||||
if (context->jni_status_code != kJniStatusOk) {
|
if (context->jni_status_code != kJniStatusOk) {
|
||||||
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr int AlignTo16(int value) { return (value + 15) & (~15); }
|
constexpr int AlignTo16(int value) { return (value + 15) & (~15); }
|
||||||
|
|
@ -508,8 +532,8 @@ DECODER_FUNC(jlong, gav1Init, jint threads) {
|
||||||
|
|
||||||
libgav1::DecoderSettings settings;
|
libgav1::DecoderSettings settings;
|
||||||
settings.threads = threads;
|
settings.threads = threads;
|
||||||
settings.get = Libgav1GetFrameBuffer;
|
settings.get_frame_buffer = Libgav1GetFrameBuffer;
|
||||||
settings.release = Libgav1ReleaseFrameBuffer;
|
settings.release_frame_buffer = Libgav1ReleaseFrameBuffer;
|
||||||
settings.callback_private_data = context;
|
settings.callback_private_data = context;
|
||||||
|
|
||||||
context->libgav1_status_code = context->decoder.Init(&settings);
|
context->libgav1_status_code = context->decoder.Init(&settings);
|
||||||
|
|
@ -544,7 +568,8 @@ DECODER_FUNC(jint, gav1Decode, jlong jContext, jobject encodedData,
|
||||||
const uint8_t* const buffer = reinterpret_cast<const uint8_t*>(
|
const uint8_t* const buffer = reinterpret_cast<const uint8_t*>(
|
||||||
env->GetDirectBufferAddress(encodedData));
|
env->GetDirectBufferAddress(encodedData));
|
||||||
context->libgav1_status_code =
|
context->libgav1_status_code =
|
||||||
context->decoder.EnqueueFrame(buffer, length, /*user_private_data=*/0);
|
context->decoder.EnqueueFrame(buffer, length, /*user_private_data=*/0,
|
||||||
|
/*buffer_private_data=*/nullptr);
|
||||||
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
if (context->libgav1_status_code != kLibgav1StatusOk) {
|
||||||
return kStatusError;
|
return kStatusError;
|
||||||
}
|
}
|
||||||
|
|
@ -619,7 +644,7 @@ DECODER_FUNC(jint, gav1GetFrame, jlong jContext, jobject jOutputBuffer,
|
||||||
}
|
}
|
||||||
|
|
||||||
const int buffer_id =
|
const int buffer_id =
|
||||||
*reinterpret_cast<int*>(decoder_buffer->buffer_private_data);
|
*static_cast<const int*>(decoder_buffer->buffer_private_data);
|
||||||
context->buffer_manager.AddBufferReference(buffer_id);
|
context->buffer_manager.AddBufferReference(buffer_id);
|
||||||
JniFrameBuffer* const jni_buffer =
|
JniFrameBuffer* const jni_buffer =
|
||||||
context->buffer_manager.GetBuffer(buffer_id);
|
context->buffer_manager.GetBuffer(buffer_id);
|
||||||
|
|
@ -750,5 +775,9 @@ DECODER_FUNC(jint, gav1CheckError, jlong jContext) {
|
||||||
return kStatusOk;
|
return kStatusOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DECODER_FUNC(jint, gav1GetThreads) {
|
||||||
|
return gav1_jni::GetNumberOfPerformanceCoresOnline();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(b/139902005): Add functions for getting libgav1 version and build
|
// TODO(b/139902005): Add functions for getting libgav1 version and build
|
||||||
// configuration once libgav1 ABI provides this information.
|
// configuration once libgav1 ABI provides this information.
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,13 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'com.google.android.gms:play-services-cast-framework:17.0.0'
|
api 'com.google.android.gms:play-services-cast-framework:18.1.0'
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-ui')
|
implementation project(modulePrefix + 'library-ui')
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||||
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import com.google.android.exoplayer2.BasePlayer;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
|
@ -83,6 +84,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
|
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
|
||||||
|
|
||||||
private final CastContext castContext;
|
private final CastContext castContext;
|
||||||
|
private final MediaItemConverter mediaItemConverter;
|
||||||
// TODO: Allow custom implementations of CastTimelineTracker.
|
// TODO: Allow custom implementations of CastTimelineTracker.
|
||||||
private final CastTimelineTracker timelineTracker;
|
private final CastTimelineTracker timelineTracker;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
|
|
@ -110,13 +112,25 @@ public final class CastPlayer extends BasePlayer {
|
||||||
private int pendingSeekCount;
|
private int pendingSeekCount;
|
||||||
private int pendingSeekWindowIndex;
|
private int pendingSeekWindowIndex;
|
||||||
private long pendingSeekPositionMs;
|
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.
|
* @param castContext The context from which the cast session is obtained.
|
||||||
*/
|
*/
|
||||||
public CastPlayer(CastContext castContext) {
|
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.castContext = castContext;
|
||||||
|
this.mediaItemConverter = mediaItemConverter;
|
||||||
timelineTracker = new CastTimelineTracker();
|
timelineTracker = new CastTimelineTracker();
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
statusListener = new StatusListener();
|
statusListener = new StatusListener();
|
||||||
|
|
@ -143,106 +157,61 @@ public final class CastPlayer extends BasePlayer {
|
||||||
|
|
||||||
// Media Queue manipulation methods.
|
// Media Queue manipulation methods.
|
||||||
|
|
||||||
/**
|
/** @deprecated Use {@link #setMediaItems(List, int, long)} instead. */
|
||||||
* Loads a single item media queue. If no session is available, does nothing.
|
@Deprecated
|
||||||
*
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
|
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.
|
* @deprecated Use {@link #setMediaItems(List, int, long)} and {@link #setRepeatMode(int)}
|
||||||
*
|
* instead.
|
||||||
* @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
|
||||||
@Nullable
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> loadItems(
|
public PendingResult<MediaChannelResult> loadItems(
|
||||||
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
|
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
|
||||||
if (remoteMediaClient != null) {
|
return setMediaItemsInternal(items, startIndex, positionMs, repeatMode);
|
||||||
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
|
||||||
waitingForInitialTimeline = true;
|
|
||||||
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
|
|
||||||
positionMs, null);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @deprecated Use {@link #addMediaItems(List)} instead. */
|
||||||
* Appends a sequence of items to the media queue. If no media queue exists, does nothing.
|
@Deprecated
|
||||||
*
|
|
||||||
* @param items The items to append.
|
|
||||||
* @return The Cast {@code PendingResult}, or null if no media queue exists.
|
|
||||||
*/
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
|
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
|
||||||
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
|
return addMediaItemsInternal(items, MediaQueueItem.INVALID_ITEM_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @deprecated Use {@link #addMediaItems(int, List)} instead. */
|
||||||
* Inserts a sequence of items into the media queue. If no media queue or period with id {@code
|
@Deprecated
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
|
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
|
||||||
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|
if (periodId == MediaQueueItem.INVALID_ITEM_ID
|
||||||
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
|
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||||
return remoteMediaClient.queueInsertItems(items, periodId, null);
|
return addMediaItemsInternal(items, periodId);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @deprecated Use {@link #removeMediaItem(int)} instead. */
|
||||||
* Removes an item from the media queue. If no media queue or period with id {@code periodId}
|
@Deprecated
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> removeItem(int periodId) {
|
public PendingResult<MediaChannelResult> removeItem(int periodId) {
|
||||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
if (currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||||
return remoteMediaClient.queueRemoveItem(periodId, null);
|
return removeMediaItemsInternal(new int[] {periodId});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @deprecated Use {@link #moveMediaItem(int, int)} instead. */
|
||||||
* Moves an existing item within the media queue. If no media queue or period with id {@code
|
@Deprecated
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
|
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
|
||||||
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
|
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getWindowCount());
|
||||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
int fromIndex = currentTimeline.getIndexOfPeriod(periodId);
|
||||||
return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null);
|
if (fromIndex != C.INDEX_UNSET && fromIndex != newIndex) {
|
||||||
|
return moveMediaItemsInternal(new int[] {periodId}, fromIndex, newIndex);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -307,6 +276,13 @@ public final class CastPlayer extends BasePlayer {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public DeviceComponent getDeviceComponent() {
|
||||||
|
// TODO(b/151792305): Implement the component.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Looper getApplicationLooper() {
|
public Looper getApplicationLooper() {
|
||||||
return Looper.getMainLooper();
|
return Looper.getMainLooper();
|
||||||
|
|
@ -327,6 +303,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 = Math.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
|
@Override
|
||||||
@Player.State
|
@Player.State
|
||||||
public int getPlaybackState() {
|
public int getPlaybackState() {
|
||||||
|
|
@ -339,9 +382,16 @@ public final class CastPlayer extends BasePlayer {
|
||||||
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
|
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public ExoPlaybackException getPlaybackError() {
|
public ExoPlaybackException getPlaybackError() {
|
||||||
|
return getPlayerError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public ExoPlaybackException getPlayerError() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -353,7 +403,8 @@ public final class CastPlayer extends BasePlayer {
|
||||||
// We update the local state and send the message to the receiver app, which will cause the
|
// 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,
|
// 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.
|
// 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();
|
flushNotifications();
|
||||||
PendingResult<MediaChannelResult> pendingResult =
|
PendingResult<MediaChannelResult> pendingResult =
|
||||||
playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
|
playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
|
||||||
|
|
@ -400,16 +451,32 @@ public final class CastPlayer extends BasePlayer {
|
||||||
flushNotifications();
|
flushNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
@Override
|
@Override
|
||||||
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
|
||||||
// Unsupported by the RemoteMediaClient API. Do nothing.
|
// Unsupported by the RemoteMediaClient API. Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
@Override
|
@Override
|
||||||
public PlaybackParameters getPlaybackParameters() {
|
public PlaybackParameters getPlaybackParameters() {
|
||||||
return PlaybackParameters.DEFAULT;
|
return PlaybackParameters.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPlaybackSpeed(float playbackSpeed) {
|
||||||
|
// Unsupported by the RemoteMediaClient API. Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getPlaybackSpeed() {
|
||||||
|
return Player.DEFAULT_PLAYBACK_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop(boolean reset) {
|
public void stop(boolean reset) {
|
||||||
playbackState = STATE_IDLE;
|
playbackState = STATE_IDLE;
|
||||||
|
|
@ -627,8 +694,14 @@ public final class CastPlayer extends BasePlayer {
|
||||||
newPlayWhenReadyValue = !remoteMediaClient.isPaused();
|
newPlayWhenReadyValue = !remoteMediaClient.isPaused();
|
||||||
playWhenReady.clearPendingResultCallback();
|
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.
|
// 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")
|
@RequiresNonNull("remoteMediaClient")
|
||||||
|
|
@ -641,15 +714,13 @@ public final class CastPlayer extends BasePlayer {
|
||||||
|
|
||||||
private void updateTimelineAndNotifyIfChanged() {
|
private void updateTimelineAndNotifyIfChanged() {
|
||||||
if (updateTimeline()) {
|
if (updateTimeline()) {
|
||||||
@Player.TimelineChangeReason
|
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
|
||||||
int reason =
|
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
|
||||||
waitingForInitialTimeline
|
|
||||||
? Player.TIMELINE_CHANGE_REASON_PREPARED
|
|
||||||
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
|
||||||
waitingForInitialTimeline = false;
|
|
||||||
notificationsBatch.add(
|
notificationsBatch.add(
|
||||||
new ListenerNotificationTask(
|
new ListenerNotificationTask(
|
||||||
listener -> listener.onTimelineChanged(currentTimeline, reason)));
|
listener ->
|
||||||
|
listener.onTimelineChanged(
|
||||||
|
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -713,6 +784,58 @@ public final class CastPlayer extends BasePlayer {
|
||||||
return false;
|
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,
|
||||||
|
Math.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) {
|
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
|
||||||
if (this.repeatMode.value != repeatMode) {
|
if (this.repeatMode.value != repeatMode) {
|
||||||
this.repeatMode.value = repeatMode;
|
this.repeatMode.value = repeatMode;
|
||||||
|
|
@ -721,14 +844,27 @@ public final class CastPlayer extends BasePlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
private void setPlayerStateAndNotifyIfChanged(
|
private void setPlayerStateAndNotifyIfChanged(
|
||||||
boolean playWhenReady, @Player.State int playbackState) {
|
boolean playWhenReady,
|
||||||
if (this.playWhenReady.value != playWhenReady || this.playbackState != playbackState) {
|
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
|
||||||
this.playWhenReady.value = playWhenReady;
|
@Player.State int playbackState) {
|
||||||
|
boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady;
|
||||||
|
boolean playbackStateChanged = this.playbackState != playbackState;
|
||||||
|
if (playWhenReadyChanged || playbackStateChanged) {
|
||||||
this.playbackState = playbackState;
|
this.playbackState = playbackState;
|
||||||
|
this.playWhenReady.value = playWhenReady;
|
||||||
notificationsBatch.add(
|
notificationsBatch.add(
|
||||||
new ListenerNotificationTask(
|
new ListenerNotificationTask(
|
||||||
listener -> listener.onPlayerStateChanged(playWhenReady, playbackState)));
|
listener -> {
|
||||||
|
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||||
|
if (playbackStateChanged) {
|
||||||
|
listener.onPlaybackStateChanged(playbackState);
|
||||||
|
}
|
||||||
|
if (playWhenReadyChanged) {
|
||||||
|
listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason);
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -750,6 +886,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
|
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
|
||||||
updateInternalStateAndNotifyIfChanged();
|
updateInternalStateAndNotifyIfChanged();
|
||||||
} else {
|
} else {
|
||||||
|
updateTimelineAndNotifyIfChanged();
|
||||||
if (sessionAvailabilityListener != null) {
|
if (sessionAvailabilityListener != null) {
|
||||||
sessionAvailabilityListener.onCastSessionUnavailable();
|
sessionAvailabilityListener.onCastSessionUnavailable();
|
||||||
}
|
}
|
||||||
|
|
@ -849,6 +986,14 @@ 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.
|
// Internal classes.
|
||||||
|
|
||||||
private final class StatusListener
|
private final class StatusListener
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ import java.util.Arrays;
|
||||||
/* manifest= */ null,
|
/* manifest= */ null,
|
||||||
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
/* presentationStartTimeMs= */ C.TIME_UNSET,
|
||||||
/* windowStartTimeMs= */ C.TIME_UNSET,
|
/* windowStartTimeMs= */ C.TIME_UNSET,
|
||||||
|
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
|
||||||
/* isSeekable= */ !isDynamic,
|
/* isSeekable= */ !isDynamic,
|
||||||
isDynamic,
|
isDynamic,
|
||||||
isLive[windowIndex],
|
isLive[windowIndex],
|
||||||
|
|
|
||||||
|
|
@ -104,16 +104,11 @@ import com.google.android.gms.cast.MediaTrack;
|
||||||
* @return The equivalent {@link Format}.
|
* @return The equivalent {@link Format}.
|
||||||
*/
|
*/
|
||||||
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
|
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
|
||||||
return Format.createContainerFormat(
|
return new Format.Builder()
|
||||||
mediaTrack.getContentId(),
|
.setId(mediaTrack.getContentId())
|
||||||
/* label= */ null,
|
.setContainerMimeType(mediaTrack.getContentType())
|
||||||
mediaTrack.getContentType(),
|
.setLanguage(mediaTrack.getLanguage())
|
||||||
/* sampleMimeType= */ null,
|
.build();
|
||||||
/* codecs= */ null,
|
|
||||||
/* bitrate= */ Format.NO_VALUE,
|
|
||||||
/* selectionFlags= */ 0,
|
|
||||||
/* roleFlags= */ 0,
|
|
||||||
mediaTrack.getLanguage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CastUtils() {}
|
private CastUtils() {}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ package com.google.android.exoplayer2.ext.cast;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
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.MediaInfo;
|
||||||
import com.google.android.gms.cast.MediaMetadata;
|
import com.google.android.gms.cast.MediaMetadata;
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
import com.google.android.gms.cast.MediaQueueItem;
|
||||||
|
|
@ -43,22 +44,24 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaItem toMediaItem(MediaQueueItem item) {
|
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
|
@Override
|
||||||
public MediaQueueItem toMediaQueueItem(MediaItem item) {
|
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");
|
throw new IllegalArgumentException("The item must specify its mimeType");
|
||||||
}
|
}
|
||||||
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
||||||
if (item.title != null) {
|
if (item.mediaMetadata.title != null) {
|
||||||
metadata.putString(MediaMetadata.KEY_TITLE, item.title);
|
metadata.putString(MediaMetadata.KEY_TITLE, item.mediaMetadata.title);
|
||||||
}
|
}
|
||||||
MediaInfo mediaInfo =
|
MediaInfo mediaInfo =
|
||||||
new MediaInfo.Builder(item.uri.toString())
|
new MediaInfo.Builder(item.playbackProperties.uri.toString())
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||||
.setContentType(item.mimeType)
|
.setContentType(item.playbackProperties.mimeType)
|
||||||
.setMetadata(metadata)
|
.setMetadata(metadata)
|
||||||
.setCustomData(getCustomData(item))
|
.setCustomData(getCustomData(item))
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -73,14 +76,17 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||||
MediaItem.Builder builder = new MediaItem.Builder();
|
MediaItem.Builder builder = new MediaItem.Builder();
|
||||||
builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
|
builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
|
||||||
if (mediaItemJson.has(KEY_TITLE)) {
|
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)) {
|
if (mediaItemJson.has(KEY_MIME_TYPE)) {
|
||||||
builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
|
builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
|
||||||
}
|
}
|
||||||
if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) {
|
if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) {
|
||||||
builder.setDrmConfiguration(
|
populateDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION), builder);
|
||||||
getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION)));
|
|
||||||
}
|
}
|
||||||
return builder.build();
|
return builder.build();
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
|
|
@ -88,25 +94,26 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException {
|
private static void populateDrmConfiguration(JSONObject json, MediaItem.Builder builder)
|
||||||
UUID uuid = UUID.fromString(json.getString(KEY_UUID));
|
throws JSONException {
|
||||||
Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI));
|
builder.setDrmUuid(UUID.fromString(json.getString(KEY_UUID)));
|
||||||
|
builder.setDrmLicenseUri(json.getString(KEY_LICENSE_URI));
|
||||||
JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS);
|
JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS);
|
||||||
HashMap<String, String> requestHeaders = new HashMap<>();
|
HashMap<String, String> requestHeaders = new HashMap<>();
|
||||||
for (Iterator<String> iterator = requestHeadersJson.keys(); iterator.hasNext(); ) {
|
for (Iterator<String> iterator = requestHeadersJson.keys(); iterator.hasNext(); ) {
|
||||||
String key = iterator.next();
|
String key = iterator.next();
|
||||||
requestHeaders.put(key, requestHeadersJson.getString(key));
|
requestHeaders.put(key, requestHeadersJson.getString(key));
|
||||||
}
|
}
|
||||||
return new DrmConfiguration(uuid, licenseUri, requestHeaders);
|
builder.setDrmLicenseRequestHeaders(requestHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialization.
|
// Serialization.
|
||||||
|
|
||||||
private static JSONObject getCustomData(MediaItem item) {
|
private static JSONObject getCustomData(MediaItem mediaItem) {
|
||||||
JSONObject json = new JSONObject();
|
JSONObject json = new JSONObject();
|
||||||
try {
|
try {
|
||||||
json.put(KEY_MEDIA_ITEM, getMediaItemJson(item));
|
json.put(KEY_MEDIA_ITEM, getMediaItemJson(mediaItem));
|
||||||
JSONObject playerConfigJson = getPlayerConfigJson(item);
|
@Nullable JSONObject playerConfigJson = getPlayerConfigJson(mediaItem);
|
||||||
if (playerConfigJson != null) {
|
if (playerConfigJson != null) {
|
||||||
json.put(KEY_PLAYER_CONFIG, playerConfigJson);
|
json.put(KEY_PLAYER_CONFIG, playerConfigJson);
|
||||||
}
|
}
|
||||||
|
|
@ -116,18 +123,21 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||||
return json;
|
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();
|
JSONObject json = new JSONObject();
|
||||||
json.put(KEY_URI, item.uri.toString());
|
json.put(KEY_TITLE, mediaItem.mediaMetadata.title);
|
||||||
json.put(KEY_TITLE, item.title);
|
json.put(KEY_URI, mediaItem.playbackProperties.uri.toString());
|
||||||
json.put(KEY_MIME_TYPE, item.mimeType);
|
json.put(KEY_MIME_TYPE, mediaItem.playbackProperties.mimeType);
|
||||||
if (item.drmConfiguration != null) {
|
if (mediaItem.playbackProperties.drmConfiguration != null) {
|
||||||
json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration));
|
json.put(
|
||||||
|
KEY_DRM_CONFIGURATION,
|
||||||
|
getDrmConfigurationJson(mediaItem.playbackProperties.drmConfiguration));
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration)
|
private static JSONObject getDrmConfigurationJson(MediaItem.DrmConfiguration drmConfiguration)
|
||||||
throws JSONException {
|
throws JSONException {
|
||||||
JSONObject json = new JSONObject();
|
JSONObject json = new JSONObject();
|
||||||
json.put(KEY_UUID, drmConfiguration.uuid);
|
json.put(KEY_UUID, drmConfiguration.uuid);
|
||||||
|
|
@ -137,11 +147,12 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException {
|
private static JSONObject getPlayerConfigJson(MediaItem mediaItem) throws JSONException {
|
||||||
DrmConfiguration drmConfiguration = item.drmConfiguration;
|
if (mediaItem.playbackProperties == null
|
||||||
if (drmConfiguration == null) {
|
|| mediaItem.playbackProperties.drmConfiguration == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
|
||||||
|
|
||||||
String drmScheme;
|
String drmScheme;
|
||||||
if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) {
|
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;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
import com.google.android.gms.cast.MediaQueueItem;
|
||||||
|
|
||||||
/** Converts between {@link MediaItem} and the Cast SDK's {@link 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 com.google.common.truth.Truth.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
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.verify;
|
||||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.mockito.MockitoAnnotations.initMocks;
|
import static org.mockito.MockitoAnnotations.initMocks;
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
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.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.MediaStatus;
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
import com.google.android.gms.cast.framework.CastSession;
|
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.cast.framework.media.RemoteMediaClient;
|
||||||
import com.google.android.gms.common.api.PendingResult;
|
import com.google.android.gms.common.api.PendingResult;
|
||||||
import com.google.android.gms.common.api.ResultCallback;
|
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.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
@ -46,9 +58,13 @@ import org.mockito.Mockito;
|
||||||
public class CastPlayerTest {
|
public class CastPlayerTest {
|
||||||
|
|
||||||
private CastPlayer castPlayer;
|
private CastPlayer castPlayer;
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
private RemoteMediaClient.Listener remoteMediaClientListener;
|
private RemoteMediaClient.Listener remoteMediaClientListener;
|
||||||
|
|
||||||
@Mock private RemoteMediaClient mockRemoteMediaClient;
|
@Mock private RemoteMediaClient mockRemoteMediaClient;
|
||||||
@Mock private MediaStatus mockMediaStatus;
|
@Mock private MediaStatus mockMediaStatus;
|
||||||
|
@Mock private MediaInfo mockMediaInfo;
|
||||||
@Mock private MediaQueue mockMediaQueue;
|
@Mock private MediaQueue mockMediaQueue;
|
||||||
@Mock private CastContext mockCastContext;
|
@Mock private CastContext mockCastContext;
|
||||||
@Mock private SessionManager mockSessionManager;
|
@Mock private SessionManager mockSessionManager;
|
||||||
|
|
@ -62,6 +78,9 @@ public class CastPlayerTest {
|
||||||
|
|
||||||
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor;
|
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor;
|
||||||
|
|
||||||
|
@Captor private ArgumentCaptor<MediaQueueItem[]> queueItemsArgumentCaptor;
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
initMocks(this);
|
initMocks(this);
|
||||||
|
|
@ -80,15 +99,18 @@ public class CastPlayerTest {
|
||||||
remoteMediaClientListener = listenerArgumentCaptor.getValue();
|
remoteMediaClientListener = listenerArgumentCaptor.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Test
|
@Test
|
||||||
public void testSetPlayWhenReady_masksRemoteState() {
|
public void setPlayWhenReady_masksRemoteState() {
|
||||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||||
|
|
||||||
castPlayer.setPlayWhenReady(true);
|
castPlayer.play();
|
||||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
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.
|
// There is a status update in the middle, which should be hidden by masking.
|
||||||
remoteMediaClientListener.onStatusUpdated();
|
remoteMediaClientListener.onStatusUpdated();
|
||||||
|
|
@ -102,35 +124,59 @@ public class CastPlayerTest {
|
||||||
verifyNoMoreInteractions(mockListener);
|
verifyNoMoreInteractions(mockListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Test
|
@Test
|
||||||
public void testSetPlayWhenReadyMasking_updatesUponResultChange() {
|
public void setPlayWhenReadyMasking_updatesUponResultChange() {
|
||||||
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
|
||||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||||
|
|
||||||
castPlayer.setPlayWhenReady(true);
|
castPlayer.play();
|
||||||
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
|
||||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
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.
|
// Upon result, the remote media client is still paused. The state should reflect that.
|
||||||
setResultCallbackArgumentCaptor
|
setResultCallbackArgumentCaptor
|
||||||
.getValue()
|
.getValue()
|
||||||
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
|
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
|
||||||
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
|
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
|
||||||
|
verify(mockListener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
|
||||||
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Test
|
@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();
|
assertThat(castPlayer.getPlayWhenReady()).isFalse();
|
||||||
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
|
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
|
||||||
remoteMediaClientListener.onStatusUpdated();
|
remoteMediaClientListener.onStatusUpdated();
|
||||||
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
|
||||||
|
verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
|
||||||
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
assertThat(castPlayer.getPlayWhenReady()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetRepeatMode_masksRemoteState() {
|
public void setRepeatMode_masksRemoteState() {
|
||||||
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
|
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
|
||||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
|
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
|
||||||
|
|
||||||
|
|
@ -153,7 +199,7 @@ public class CastPlayerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetRepeatMode_updatesUponResultChange() {
|
public void setRepeatMode_updatesUponResultChange() {
|
||||||
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
|
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
|
||||||
|
|
||||||
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
|
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
|
||||||
|
|
@ -175,11 +221,279 @@ public class CastPlayerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRepeatMode_changesOnStatusUpdates() {
|
public void repeatMode_changesOnStatusUpdates() {
|
||||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
|
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
|
||||||
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
|
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
|
||||||
remoteMediaClientListener.onStatusUpdated();
|
remoteMediaClientListener.onStatusUpdated();
|
||||||
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
|
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
|
||||||
assertThat(castPlayer.getRepeatMode()).isEqualTo(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.
|
||||||
|
remoteMediaClientListener.onQueueStatusUpdated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ public class CastTimelineTrackerTest {
|
||||||
|
|
||||||
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
||||||
@Test
|
@Test
|
||||||
public void testGetCastTimelinePersistsDuration() {
|
public void getCastTimelinePersistsDuration() {
|
||||||
CastTimelineTracker tracker = new CastTimelineTracker();
|
CastTimelineTracker tracker = new CastTimelineTracker();
|
||||||
|
|
||||||
RemoteMediaClient remoteMediaClient =
|
RemoteMediaClient remoteMediaClient =
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
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 com.google.android.gms.cast.MediaQueueItem;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -33,7 +35,8 @@ public class DefaultMediaItemConverterTest {
|
||||||
@Test
|
@Test
|
||||||
public void serialize_deserialize_minimal() {
|
public void serialize_deserialize_minimal() {
|
||||||
MediaItem.Builder builder = new MediaItem.Builder();
|
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();
|
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
||||||
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
|
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
|
||||||
|
|
@ -48,13 +51,11 @@ public class DefaultMediaItemConverterTest {
|
||||||
MediaItem item =
|
MediaItem item =
|
||||||
builder
|
builder
|
||||||
.setUri(Uri.parse("http://example.com"))
|
.setUri(Uri.parse("http://example.com"))
|
||||||
.setTitle("title")
|
.setMediaMetadata(new MediaMetadata.Builder().build())
|
||||||
.setMimeType("mime")
|
.setMimeType(MimeTypes.APPLICATION_MPD)
|
||||||
.setDrmConfiguration(
|
.setDrmUuid(C.WIDEVINE_UUID)
|
||||||
new DrmConfiguration(
|
.setDrmLicenseUri("http://license.com")
|
||||||
C.WIDEVINE_UUID,
|
.setDrmLicenseRequestHeaders(Collections.singletonMap("key", "value"))
|
||||||
Uri.parse("http://license.com"),
|
|
||||||
Collections.singletonMap("key", "value")))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -35,6 +35,7 @@ dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
testImplementation project(modulePrefix + 'library')
|
testImplementation project(modulePrefix + 'library')
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import com.google.android.exoplayer2.util.ConditionVariable;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Predicate;
|
import com.google.android.exoplayer2.util.Predicate;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
@ -83,14 +84,6 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Thrown on catching an InterruptedException. */
|
|
||||||
public static final class InterruptedIOException extends IOException {
|
|
||||||
|
|
||||||
public InterruptedIOException(InterruptedException e) {
|
|
||||||
super(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ExoPlayerLibraryInfo.registerModule("goog.exo.cronet");
|
ExoPlayerLibraryInfo.registerModule("goog.exo.cronet");
|
||||||
}
|
}
|
||||||
|
|
@ -440,7 +433,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new OpenException(new InterruptedIOException(e), dataSpec, Status.INVALID);
|
throw new OpenException(new InterruptedIOException(), dataSpec, Status.INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a valid response code.
|
// Check for a valid response code.
|
||||||
|
|
@ -705,7 +698,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) {
|
if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) {
|
||||||
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the Range header.
|
// Set the Range header.
|
||||||
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
||||||
StringBuilder rangeValue = new StringBuilder();
|
StringBuilder rangeValue = new StringBuilder();
|
||||||
|
|
@ -769,7 +762,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
}
|
}
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new HttpDataSourceException(
|
throw new HttpDataSourceException(
|
||||||
new InterruptedIOException(e),
|
new InterruptedIOException(),
|
||||||
castNonNull(currentDataSpec),
|
castNonNull(currentDataSpec),
|
||||||
HttpDataSourceException.TYPE_READ);
|
HttpDataSourceException.TYPE_READ);
|
||||||
} catch (SocketTimeoutException e) {
|
} catch (SocketTimeoutException e) {
|
||||||
|
|
@ -819,7 +812,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
try {
|
try {
|
||||||
long contentLengthFromRange =
|
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) {
|
if (contentLength < 0) {
|
||||||
// Some proxy servers strip the Content-Length header. Fall back to the length
|
// Some proxy servers strip the Content-Length header. Fall back to the length
|
||||||
// calculated here in this case.
|
// calculated here in this case.
|
||||||
|
|
@ -924,16 +919,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
|
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
|
||||||
// transformed into a GET.
|
// transformed into a GET.
|
||||||
redirectUrlDataSpec =
|
redirectUrlDataSpec =
|
||||||
new DataSpec(
|
dataSpec
|
||||||
Uri.parse(newLocationUrl),
|
.buildUpon()
|
||||||
DataSpec.HTTP_METHOD_GET,
|
.setUri(newLocationUrl)
|
||||||
/* httpBody= */ null,
|
.setHttpMethod(DataSpec.HTTP_METHOD_GET)
|
||||||
dataSpec.absoluteStreamPosition,
|
.setHttpBody(null)
|
||||||
dataSpec.position,
|
.build();
|
||||||
dataSpec.length,
|
|
||||||
dataSpec.key,
|
|
||||||
dataSpec.flags,
|
|
||||||
dataSpec.httpRequestHeaders);
|
|
||||||
} else {
|
} else {
|
||||||
redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
|
redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,8 @@ public final class CronetEngineWrapper {
|
||||||
private final boolean preferGMSCoreCronet;
|
private final boolean preferGMSCoreCronet;
|
||||||
|
|
||||||
// Multi-catch can only be used for API 19+ in this case.
|
// 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) {
|
public CronetProviderComparator(boolean preferGMSCoreCronet) {
|
||||||
// GMSCore CronetProvider classes are only available in some configurations.
|
// GMSCore CronetProvider classes are only available in some configurations.
|
||||||
// Thus, we use reflection to copy static name.
|
// Thus, we use reflection to copy static name.
|
||||||
|
|
|
||||||
|
|
@ -48,18 +48,18 @@ public final class ByteArrayUploadDataProviderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetLength() {
|
public void getLength() {
|
||||||
assertThat(byteArrayUploadDataProvider.getLength()).isEqualTo(TEST_DATA.length);
|
assertThat(byteArrayUploadDataProvider.getLength()).isEqualTo(TEST_DATA.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadFullBuffer() throws IOException {
|
public void readFullBuffer() throws IOException {
|
||||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadPartialBuffer() throws IOException {
|
public void readPartialBuffer() throws IOException {
|
||||||
byte[] firstHalf = Arrays.copyOf(TEST_DATA, TEST_DATA.length / 2);
|
byte[] firstHalf = Arrays.copyOf(TEST_DATA, TEST_DATA.length / 2);
|
||||||
byte[] secondHalf = Arrays.copyOfRange(TEST_DATA, TEST_DATA.length / 2, TEST_DATA.length);
|
byte[] secondHalf = Arrays.copyOfRange(TEST_DATA, TEST_DATA.length / 2, TEST_DATA.length);
|
||||||
byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);
|
byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);
|
||||||
|
|
@ -75,7 +75,7 @@ public final class ByteArrayUploadDataProviderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRewind() throws IOException {
|
public void rewind() throws IOException {
|
||||||
// Read all the data.
|
// Read all the data.
|
||||||
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
|
||||||
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
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.doAnswer;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
@ -40,6 +40,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
@ -63,9 +64,13 @@ import org.junit.runner.RunWith;
|
||||||
import org.mockito.ArgumentMatchers;
|
import org.mockito.ArgumentMatchers;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.robolectric.annotation.LooperMode;
|
||||||
|
import org.robolectric.annotation.LooperMode.Mode;
|
||||||
|
import org.robolectric.shadows.ShadowLooper;
|
||||||
|
|
||||||
/** Tests for {@link CronetDataSource}. */
|
/** Tests for {@link CronetDataSource}. */
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
@LooperMode(Mode.PAUSED)
|
||||||
public final class CronetDataSourceTest {
|
public final class CronetDataSourceTest {
|
||||||
|
|
||||||
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
|
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
|
||||||
|
|
@ -120,12 +125,15 @@ public final class CronetDataSourceTest {
|
||||||
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
|
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
|
||||||
mockStatusResponse();
|
mockStatusResponse();
|
||||||
|
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL));
|
||||||
testPostDataSpec =
|
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 =
|
testHeadDataSpec =
|
||||||
new DataSpec(
|
new DataSpec.Builder().setUri(TEST_URL).setHttpMethod(DataSpec.HTTP_METHOD_HEAD).build();
|
||||||
Uri.parse(TEST_URL), DataSpec.HTTP_METHOD_HEAD, null, 0, 0, C.LENGTH_UNSET, null, 0);
|
|
||||||
testResponseHeader = new HashMap<>();
|
testResponseHeader = new HashMap<>();
|
||||||
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
|
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
|
||||||
// This value can be anything since the DataSpec is unset.
|
// This value can be anything since the DataSpec is unset.
|
||||||
|
|
@ -151,7 +159,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOpeningTwiceThrows() throws HttpDataSourceException {
|
public void openingTwiceThrows() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
try {
|
try {
|
||||||
|
|
@ -163,7 +171,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCallbackFromPreviousRequest() throws HttpDataSourceException {
|
public void callbackFromPreviousRequest() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
@ -186,7 +194,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestStartCalled() throws HttpDataSourceException {
|
public void requestStartCalled() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
@ -196,8 +204,8 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestSetsRangeHeader() throws HttpDataSourceException {
|
public void requestSetsRangeHeader() throws HttpDataSourceException {
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
@ -206,8 +214,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestHeadersSet() throws HttpDataSourceException {
|
public void requestHeadersSet() throws HttpDataSourceException {
|
||||||
|
|
||||||
Map<String, String> headersSet = new HashMap<>();
|
Map<String, String> headersSet = new HashMap<>();
|
||||||
doAnswer(
|
doAnswer(
|
||||||
(invocation) -> {
|
(invocation) -> {
|
||||||
|
|
@ -227,17 +234,14 @@ public final class CronetDataSourceTest {
|
||||||
dataSpecRequestProperties.put("defaultHeader3", "dataSpecOverridesAll");
|
dataSpecRequestProperties.put("defaultHeader3", "dataSpecOverridesAll");
|
||||||
dataSpecRequestProperties.put("dataSourceHeader2", "dataSpecOverridesDataSource");
|
dataSpecRequestProperties.put("dataSourceHeader2", "dataSpecOverridesDataSource");
|
||||||
dataSpecRequestProperties.put("dataSpecHeader1", "dataSpecValue1");
|
dataSpecRequestProperties.put("dataSpecHeader1", "dataSpecValue1");
|
||||||
|
|
||||||
testDataSpec =
|
testDataSpec =
|
||||||
new DataSpec(
|
new DataSpec.Builder()
|
||||||
/* uri= */ Uri.parse(TEST_URL),
|
.setUri(TEST_URL)
|
||||||
/* httpMethod= */ DataSpec.HTTP_METHOD_GET,
|
.setPosition(1000)
|
||||||
/* httpBody= */ null,
|
.setLength(5000)
|
||||||
/* absoluteStreamPosition= */ 1000,
|
.setHttpRequestHeaders(dataSpecRequestProperties)
|
||||||
/* position= */ 1000,
|
.build();
|
||||||
/* length= */ 5000,
|
|
||||||
/* key= */ null,
|
|
||||||
/* flags= */ 0,
|
|
||||||
dataSpecRequestProperties);
|
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
@ -253,7 +257,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestOpen() throws HttpDataSourceException {
|
public void requestOpen() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
|
||||||
verify(mockTransferListener)
|
verify(mockTransferListener)
|
||||||
|
|
@ -261,9 +265,8 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestOpenGzippedCompressedReturnsDataSpecLength()
|
public void requestOpenGzippedCompressedReturnsDataSpecLength() throws HttpDataSourceException {
|
||||||
throws HttpDataSourceException {
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000);
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000, null);
|
|
||||||
testResponseHeader.put("Content-Encoding", "gzip");
|
testResponseHeader.put("Content-Encoding", "gzip");
|
||||||
testResponseHeader.put("Content-Length", Long.toString(50L));
|
testResponseHeader.put("Content-Length", Long.toString(50L));
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
@ -274,7 +277,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestOpenFail() {
|
public void requestOpenFail() {
|
||||||
mockResponseStartFailure();
|
mockResponseStartFailure();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -282,7 +285,7 @@ public final class CronetDataSourceTest {
|
||||||
fail("HttpDataSource.HttpDataSourceException expected");
|
fail("HttpDataSource.HttpDataSourceException expected");
|
||||||
} catch (HttpDataSourceException e) {
|
} catch (HttpDataSourceException e) {
|
||||||
// Check for connection not automatically closed.
|
// Check for connection not automatically closed.
|
||||||
assertThat(e.getCause() instanceof UnknownHostException).isFalse();
|
assertThat(e).hasCauseThat().isNotInstanceOf(UnknownHostException.class);
|
||||||
verify(mockUrlRequest, never()).cancel();
|
verify(mockUrlRequest, never()).cancel();
|
||||||
verify(mockTransferListener, never())
|
verify(mockTransferListener, never())
|
||||||
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
|
|
@ -292,14 +295,14 @@ public final class CronetDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
public void open_ifBodyIsSetWithoutContentTypeHeader_fails() {
|
public void open_ifBodyIsSetWithoutContentTypeHeader_fails() {
|
||||||
testDataSpec =
|
testDataSpec =
|
||||||
new DataSpec(
|
new DataSpec.Builder()
|
||||||
/* uri= */ Uri.parse(TEST_URL),
|
.setUri(TEST_URL)
|
||||||
/* postBody= */ new byte[1024],
|
.setHttpMethod(DataSpec.HTTP_METHOD_POST)
|
||||||
/* absoluteStreamPosition= */ 200,
|
.setHttpBody(new byte[1024])
|
||||||
/* position= */ 200,
|
.setPosition(200)
|
||||||
/* length= */ 1024,
|
.setLength(1024)
|
||||||
/* key= */ "key",
|
.setKey("key")
|
||||||
/* flags= */ 0);
|
.build();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
@ -310,7 +313,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestOpenFailDueToDnsFailure() {
|
public void requestOpenFailDueToDnsFailure() {
|
||||||
mockResponseStartFailure();
|
mockResponseStartFailure();
|
||||||
when(mockNetworkException.getErrorCode())
|
when(mockNetworkException.getErrorCode())
|
||||||
.thenReturn(NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
|
.thenReturn(NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
|
||||||
|
|
@ -320,7 +323,7 @@ public final class CronetDataSourceTest {
|
||||||
fail("HttpDataSource.HttpDataSourceException expected");
|
fail("HttpDataSource.HttpDataSourceException expected");
|
||||||
} catch (HttpDataSourceException e) {
|
} catch (HttpDataSourceException e) {
|
||||||
// Check for connection not automatically closed.
|
// Check for connection not automatically closed.
|
||||||
assertThat(e.getCause() instanceof UnknownHostException).isTrue();
|
assertThat(e).hasCauseThat().isInstanceOf(UnknownHostException.class);
|
||||||
verify(mockUrlRequest, never()).cancel();
|
verify(mockUrlRequest, never()).cancel();
|
||||||
verify(mockTransferListener, never())
|
verify(mockTransferListener, never())
|
||||||
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
.onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
|
|
@ -328,7 +331,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestOpenValidatesStatusCode() {
|
public void requestOpenValidatesStatusCode() {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
testUrlResponseInfo = createUrlResponseInfo(500); // statusCode
|
testUrlResponseInfo = createUrlResponseInfo(500); // statusCode
|
||||||
|
|
||||||
|
|
@ -336,7 +339,7 @@ public final class CronetDataSourceTest {
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
fail("HttpDataSource.HttpDataSourceException expected");
|
fail("HttpDataSource.HttpDataSourceException expected");
|
||||||
} catch (HttpDataSourceException e) {
|
} catch (HttpDataSourceException e) {
|
||||||
assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue();
|
assertThat(e).isInstanceOf(HttpDataSource.InvalidResponseCodeException.class);
|
||||||
// Check for connection not automatically closed.
|
// Check for connection not automatically closed.
|
||||||
verify(mockUrlRequest, never()).cancel();
|
verify(mockUrlRequest, never()).cancel();
|
||||||
verify(mockTransferListener, never())
|
verify(mockTransferListener, never())
|
||||||
|
|
@ -345,7 +348,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestOpenValidatesContentTypePredicate() {
|
public void requestOpenValidatesContentTypePredicate() {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
ArrayList<String> testedContentTypes = new ArrayList<>();
|
ArrayList<String> testedContentTypes = new ArrayList<>();
|
||||||
|
|
@ -359,7 +362,7 @@ public final class CronetDataSourceTest {
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
fail("HttpDataSource.HttpDataSourceException expected");
|
fail("HttpDataSource.HttpDataSourceException expected");
|
||||||
} catch (HttpDataSourceException e) {
|
} catch (HttpDataSourceException e) {
|
||||||
assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
|
assertThat(e).isInstanceOf(HttpDataSource.InvalidContentTypeException.class);
|
||||||
// Check for connection not automatically closed.
|
// Check for connection not automatically closed.
|
||||||
verify(mockUrlRequest, never()).cancel();
|
verify(mockUrlRequest, never()).cancel();
|
||||||
assertThat(testedContentTypes).hasSize(1);
|
assertThat(testedContentTypes).hasSize(1);
|
||||||
|
|
@ -368,7 +371,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPostRequestOpen() throws HttpDataSourceException {
|
public void postRequestOpen() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||||
|
|
@ -378,7 +381,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPostRequestOpenValidatesContentType() {
|
public void postRequestOpenValidatesContentType() {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -390,7 +393,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPostRequestOpenRejects307Redirects() {
|
public void postRequestOpenRejects307Redirects() {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockResponseStartRedirect();
|
mockResponseStartRedirect();
|
||||||
|
|
||||||
|
|
@ -404,7 +407,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHeadRequestOpen() throws HttpDataSourceException {
|
public void headRequestOpen() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
dataSourceUnderTest.open(testHeadDataSpec);
|
dataSourceUnderTest.open(testHeadDataSpec);
|
||||||
verify(mockTransferListener)
|
verify(mockTransferListener)
|
||||||
|
|
@ -413,7 +416,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestReadTwice() throws HttpDataSourceException {
|
public void requestReadTwice() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
|
@ -436,7 +439,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSecondRequestNoContentLength() throws HttpDataSourceException {
|
public void secondRequestNoContentLength() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
testResponseHeader.put("Content-Length", Long.toString(1L));
|
testResponseHeader.put("Content-Length", Long.toString(1L));
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
@ -462,7 +465,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadWithOffset() throws HttpDataSourceException {
|
public void readWithOffset() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
|
@ -477,11 +480,11 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRangeRequestWith206Response() throws HttpDataSourceException {
|
public void rangeRequestWith206Response() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(1000, 5000);
|
mockReadSuccess(1000, 5000);
|
||||||
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
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);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
|
@ -494,11 +497,11 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRangeRequestWith200Response() throws HttpDataSourceException {
|
public void rangeRequestWith200Response() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 7000);
|
mockReadSuccess(0, 7000);
|
||||||
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
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);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
|
@ -511,7 +514,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadWithUnsetLength() throws HttpDataSourceException {
|
public void readWithUnsetLength() throws HttpDataSourceException {
|
||||||
testResponseHeader.remove("Content-Length");
|
testResponseHeader.remove("Content-Length");
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
@ -527,7 +530,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadReturnsWhatItCan() throws HttpDataSourceException {
|
public void readReturnsWhatItCan() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
|
@ -542,7 +545,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testClosedMeansClosed() throws HttpDataSourceException {
|
public void closedMeansClosed() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
|
@ -570,8 +573,8 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOverread() throws HttpDataSourceException {
|
public void overread() throws HttpDataSourceException {
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16);
|
||||||
testResponseHeader.put("Content-Length", Long.toString(16L));
|
testResponseHeader.put("Content-Length", Long.toString(16L));
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
@ -623,7 +626,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestReadByteBufferTwice() throws HttpDataSourceException {
|
public void requestReadByteBufferTwice() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
|
@ -649,7 +652,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRequestIntermixRead() throws HttpDataSourceException {
|
public void requestIntermixRead() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
// Chunking reads into parts 6, 7, 8, 9.
|
// Chunking reads into parts 6, 7, 8, 9.
|
||||||
mockReadSuccess(0, 30);
|
mockReadSuccess(0, 30);
|
||||||
|
|
@ -691,7 +694,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSecondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
|
public void secondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
testResponseHeader.put("Content-Length", Long.toString(1L));
|
testResponseHeader.put("Content-Length", Long.toString(1L));
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
@ -720,11 +723,11 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
|
public void rangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(1000, 5000);
|
mockReadSuccess(1000, 5000);
|
||||||
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
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);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
|
@ -738,12 +741,12 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
|
public void rangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||||
// Tests for skipping bytes.
|
// Tests for skipping bytes.
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 7000);
|
mockReadSuccess(0, 7000);
|
||||||
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
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);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
|
@ -757,7 +760,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadByteBufferWithUnsetLength() throws HttpDataSourceException {
|
public void readByteBufferWithUnsetLength() throws HttpDataSourceException {
|
||||||
testResponseHeader.remove("Content-Length");
|
testResponseHeader.remove("Content-Length");
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
@ -775,7 +778,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadByteBufferReturnsWhatItCan() throws HttpDataSourceException {
|
public void readByteBufferReturnsWhatItCan() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
|
@ -791,8 +794,8 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOverreadByteBuffer() throws HttpDataSourceException {
|
public void overreadByteBuffer() throws HttpDataSourceException {
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16);
|
||||||
testResponseHeader.put("Content-Length", Long.toString(16L));
|
testResponseHeader.put("Content-Length", Long.toString(16L));
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
@ -847,7 +850,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testClosedMeansClosedReadByteBuffer() throws HttpDataSourceException {
|
public void closedMeansClosedReadByteBuffer() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadSuccess(0, 16);
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
|
@ -877,7 +880,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConnectTimeout() throws InterruptedException {
|
public void connectTimeout() throws InterruptedException {
|
||||||
long startTimeMs = SystemClock.elapsedRealtime();
|
long startTimeMs = SystemClock.elapsedRealtime();
|
||||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||||
|
|
@ -890,8 +893,8 @@ public final class CronetDataSourceTest {
|
||||||
fail();
|
fail();
|
||||||
} catch (HttpDataSourceException e) {
|
} catch (HttpDataSourceException e) {
|
||||||
// Expected.
|
// Expected.
|
||||||
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
|
assertThat(e).isInstanceOf(CronetDataSource.OpenException.class);
|
||||||
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
|
assertThat(e).hasCauseThat().isInstanceOf(SocketTimeoutException.class);
|
||||||
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
|
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
|
||||||
.isEqualTo(TEST_CONNECTION_STATUS);
|
.isEqualTo(TEST_CONNECTION_STATUS);
|
||||||
timedOutLatch.countDown();
|
timedOutLatch.countDown();
|
||||||
|
|
@ -903,10 +906,12 @@ public final class CronetDataSourceTest {
|
||||||
// We should still be trying to open.
|
// We should still be trying to open.
|
||||||
assertNotCountedDown(timedOutLatch);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// We should still be trying to open as we approach the timeout.
|
// 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);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// Now we timeout.
|
// Now we timeout.
|
||||||
SystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
|
setSystemClockInMsAndTriggerPendingMessages(
|
||||||
|
/* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
|
||||||
timedOutLatch.await();
|
timedOutLatch.await();
|
||||||
|
|
||||||
verify(mockTransferListener, never())
|
verify(mockTransferListener, never())
|
||||||
|
|
@ -914,7 +919,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConnectInterrupted() throws InterruptedException {
|
public void connectInterrupted() throws InterruptedException {
|
||||||
long startTimeMs = SystemClock.elapsedRealtime();
|
long startTimeMs = SystemClock.elapsedRealtime();
|
||||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||||
|
|
@ -928,8 +933,8 @@ public final class CronetDataSourceTest {
|
||||||
fail();
|
fail();
|
||||||
} catch (HttpDataSourceException e) {
|
} catch (HttpDataSourceException e) {
|
||||||
// Expected.
|
// Expected.
|
||||||
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
|
assertThat(e).isInstanceOf(CronetDataSource.OpenException.class);
|
||||||
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
|
assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class);
|
||||||
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
|
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
|
||||||
.isEqualTo(TEST_INVALID_CONNECTION_STATUS);
|
.isEqualTo(TEST_INVALID_CONNECTION_STATUS);
|
||||||
timedOutLatch.countDown();
|
timedOutLatch.countDown();
|
||||||
|
|
@ -942,7 +947,8 @@ public final class CronetDataSourceTest {
|
||||||
// We should still be trying to open.
|
// We should still be trying to open.
|
||||||
assertNotCountedDown(timedOutLatch);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// We should still be trying to open as we approach the timeout.
|
// 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);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// Now we interrupt.
|
// Now we interrupt.
|
||||||
thread.interrupt();
|
thread.interrupt();
|
||||||
|
|
@ -953,7 +959,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConnectResponseBeforeTimeout() throws Exception {
|
public void connectResponseBeforeTimeout() throws Exception {
|
||||||
long startTimeMs = SystemClock.elapsedRealtime();
|
long startTimeMs = SystemClock.elapsedRealtime();
|
||||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||||
final CountDownLatch openLatch = new CountDownLatch(1);
|
final CountDownLatch openLatch = new CountDownLatch(1);
|
||||||
|
|
@ -976,7 +982,8 @@ public final class CronetDataSourceTest {
|
||||||
// We should still be trying to open.
|
// We should still be trying to open.
|
||||||
assertNotCountedDown(openLatch);
|
assertNotCountedDown(openLatch);
|
||||||
// We should still be trying to open as we approach the timeout.
|
// 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);
|
assertNotCountedDown(openLatch);
|
||||||
// The response arrives just in time.
|
// The response arrives just in time.
|
||||||
dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
|
||||||
|
|
@ -985,7 +992,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRedirectIncreasesConnectionTimeout() throws Exception {
|
public void redirectIncreasesConnectionTimeout() throws Exception {
|
||||||
long startTimeMs = SystemClock.elapsedRealtime();
|
long startTimeMs = SystemClock.elapsedRealtime();
|
||||||
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
final ConditionVariable startCondition = buildUrlRequestStartedCondition();
|
||||||
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||||
|
|
@ -999,8 +1006,8 @@ public final class CronetDataSourceTest {
|
||||||
fail();
|
fail();
|
||||||
} catch (HttpDataSourceException e) {
|
} catch (HttpDataSourceException e) {
|
||||||
// Expected.
|
// Expected.
|
||||||
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
|
assertThat(e).isInstanceOf(CronetDataSource.OpenException.class);
|
||||||
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
|
assertThat(e).hasCauseThat().isInstanceOf(SocketTimeoutException.class);
|
||||||
openExceptions.getAndIncrement();
|
openExceptions.getAndIncrement();
|
||||||
timedOutLatch.countDown();
|
timedOutLatch.countDown();
|
||||||
}
|
}
|
||||||
|
|
@ -1011,14 +1018,15 @@ public final class CronetDataSourceTest {
|
||||||
// We should still be trying to open.
|
// We should still be trying to open.
|
||||||
assertNotCountedDown(timedOutLatch);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// We should still be trying to open as we approach the timeout.
|
// 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);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// A redirect arrives just in time.
|
// A redirect arrives just in time.
|
||||||
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
|
||||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
|
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");
|
||||||
|
|
||||||
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
|
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.
|
// We should still be trying to open as we approach the new timeout.
|
||||||
assertNotCountedDown(timedOutLatch);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// A redirect arrives just in time.
|
// A redirect arrives just in time.
|
||||||
|
|
@ -1026,11 +1034,11 @@ public final class CronetDataSourceTest {
|
||||||
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
|
mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");
|
||||||
|
|
||||||
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
|
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.
|
// We should still be trying to open as we approach the new timeout.
|
||||||
assertNotCountedDown(timedOutLatch);
|
assertNotCountedDown(timedOutLatch);
|
||||||
// Now we timeout.
|
// Now we timeout.
|
||||||
SystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10);
|
setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs + 10);
|
||||||
timedOutLatch.await();
|
timedOutLatch.await();
|
||||||
|
|
||||||
verify(mockTransferListener, never())
|
verify(mockTransferListener, never())
|
||||||
|
|
@ -1039,7 +1047,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRedirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect()
|
public void redirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect()
|
||||||
throws HttpDataSourceException {
|
throws HttpDataSourceException {
|
||||||
mockSingleRedirectSuccess();
|
mockSingleRedirectSuccess();
|
||||||
mockFollowRedirectSuccess();
|
mockFollowRedirectSuccess();
|
||||||
|
|
@ -1084,7 +1092,7 @@ public final class CronetDataSourceTest {
|
||||||
public void
|
public void
|
||||||
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
|
testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
|
||||||
throws HttpDataSourceException {
|
throws HttpDataSourceException {
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||||
dataSourceUnderTest =
|
dataSourceUnderTest =
|
||||||
new CronetDataSource(
|
new CronetDataSource(
|
||||||
mockCronetEngine,
|
mockCronetEngine,
|
||||||
|
|
@ -1111,7 +1119,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRedirectNoSetCookieFollowsRedirect() throws HttpDataSourceException {
|
public void redirectNoSetCookieFollowsRedirect() throws HttpDataSourceException {
|
||||||
mockSingleRedirectSuccess();
|
mockSingleRedirectSuccess();
|
||||||
mockFollowRedirectSuccess();
|
mockFollowRedirectSuccess();
|
||||||
|
|
||||||
|
|
@ -1121,7 +1129,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
public void redirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
|
||||||
throws HttpDataSourceException {
|
throws HttpDataSourceException {
|
||||||
dataSourceUnderTest =
|
dataSourceUnderTest =
|
||||||
new CronetDataSource(
|
new CronetDataSource(
|
||||||
|
|
@ -1143,7 +1151,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExceptionFromTransferListener() throws HttpDataSourceException {
|
public void exceptionFromTransferListener() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
// Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that
|
// Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that
|
||||||
|
|
@ -1163,7 +1171,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadFailure() throws HttpDataSourceException {
|
public void readFailure() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadFailure();
|
mockReadFailure();
|
||||||
|
|
||||||
|
|
@ -1178,7 +1186,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadByteBufferFailure() throws HttpDataSourceException {
|
public void readByteBufferFailure() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadFailure();
|
mockReadFailure();
|
||||||
|
|
||||||
|
|
@ -1193,7 +1201,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadNonDirectedByteBufferFailure() throws HttpDataSourceException {
|
public void readNonDirectedByteBufferFailure() throws HttpDataSourceException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
mockReadFailure();
|
mockReadFailure();
|
||||||
|
|
||||||
|
|
@ -1208,7 +1216,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
|
public void readInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
|
@ -1224,7 +1232,7 @@ public final class CronetDataSourceTest {
|
||||||
fail();
|
fail();
|
||||||
} catch (HttpDataSourceException e) {
|
} catch (HttpDataSourceException e) {
|
||||||
// Expected.
|
// Expected.
|
||||||
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
|
assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class);
|
||||||
timedOutLatch.countDown();
|
timedOutLatch.countDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1239,7 +1247,7 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
|
public void readByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
|
@ -1255,7 +1263,7 @@ public final class CronetDataSourceTest {
|
||||||
fail();
|
fail();
|
||||||
} catch (HttpDataSourceException e) {
|
} catch (HttpDataSourceException e) {
|
||||||
// Expected.
|
// Expected.
|
||||||
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
|
assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class);
|
||||||
timedOutLatch.countDown();
|
timedOutLatch.countDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1270,8 +1278,8 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAllowDirectExecutor() throws HttpDataSourceException {
|
public void allowDirectExecutor() throws HttpDataSourceException {
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
@ -1460,4 +1468,9 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void setSystemClockInMsAndTriggerPendingMessages(long nowMs) {
|
||||||
|
SystemClock.setCurrentTimeMillis(nowMs);
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,22 +35,22 @@ FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni"
|
||||||
NDK_PATH="<path to Android NDK>"
|
NDK_PATH="<path to Android NDK>"
|
||||||
```
|
```
|
||||||
|
|
||||||
* Set up host platform ("darwin-x86_64" for Mac OS X):
|
* Set the host platform (use "darwin-x86_64" for Mac OS X):
|
||||||
|
|
||||||
```
|
```
|
||||||
HOST_PLATFORM="linux-x86_64"
|
HOST_PLATFORM="linux-x86_64"
|
||||||
```
|
```
|
||||||
|
|
||||||
* Configure the formats supported by adapting the following variable if needed
|
* Configure the decoders to include. See the [Supported formats][] page for
|
||||||
and by setting it. See the [Supported formats][] page for more details of the
|
details of the available decoders, and which formats they support.
|
||||||
formats.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
ENABLED_DECODERS=(vorbis opus flac)
|
ENABLED_DECODERS=(vorbis opus flac)
|
||||||
```
|
```
|
||||||
|
|
||||||
* Fetch and build FFmpeg. For example, executing script `build_ffmpeg.sh` will
|
* Fetch and build FFmpeg. Executing `build_ffmpeg.sh` will fetch and build
|
||||||
fetch and build FFmpeg release 4.2 for armeabi-v7a, arm64-v8a and x86:
|
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.
|
||||||
|
|
||||||
```
|
```
|
||||||
cd "${FFMPEG_EXT_PATH}" && \
|
cd "${FFMPEG_EXT_PATH}" && \
|
||||||
|
|
@ -63,7 +63,7 @@ cd "${FFMPEG_EXT_PATH}" && \
|
||||||
|
|
||||||
```
|
```
|
||||||
cd "${FFMPEG_EXT_PATH}" && \
|
cd "${FFMPEG_EXT_PATH}" && \
|
||||||
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
|
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86 x86_64" -j4
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build instructions (Windows) ##
|
## Build instructions (Windows) ##
|
||||||
|
|
@ -106,9 +106,19 @@ then implement your own logic to use the renderer for a given track.
|
||||||
[#2781]: https://github.com/google/ExoPlayer/issues/2781
|
[#2781]: https://github.com/google/ExoPlayer/issues/2781
|
||||||
[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension
|
[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension
|
||||||
|
|
||||||
|
## Using the extension in the demo application ##
|
||||||
|
|
||||||
|
To try out playback using the extension in the [demo application][], see
|
||||||
|
[enabling extension decoders][].
|
||||||
|
|
||||||
|
[demo application]: https://exoplayer.dev/demo-application.html
|
||||||
|
[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
|
||||||
|
|
||||||
## Links ##
|
## Links ##
|
||||||
|
|
||||||
|
* [Troubleshooting using extensions][]
|
||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
|
[Troubleshooting using extensions]: https://exoplayer.dev/troubleshooting.html#how-can-i-get-a-decoding-extension-to-load-and-be-used-for-playback
|
||||||
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,18 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/** FFmpeg audio decoder. */
|
||||||
* FFmpeg audio decoder.
|
/* package */ final class FfmpegAudioDecoder
|
||||||
*/
|
extends SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
|
||||||
/* package */ final class FfmpegDecoder extends
|
|
||||||
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
|
|
||||||
|
|
||||||
// Output buffer sizes when decoding PCM mu-law streams, which is the maximum FFmpeg outputs.
|
// 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_16BIT = 65536;
|
||||||
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
||||||
|
|
||||||
// Error codes matching ffmpeg_jni.cc.
|
// LINT.IfChange
|
||||||
private static final int DECODER_ERROR_INVALID_DATA = -1;
|
private static final int AUDIO_DECODER_ERROR_INVALID_DATA = -1;
|
||||||
private static final int DECODER_ERROR_OTHER = -2;
|
private static final int AUDIO_DECODER_ERROR_OTHER = -2;
|
||||||
|
// LINT.ThenChange(../../../../../../../jni/ffmpeg_jni.cc)
|
||||||
|
|
||||||
private final String codecName;
|
private final String codecName;
|
||||||
@Nullable private final byte[] extraData;
|
@Nullable private final byte[] extraData;
|
||||||
|
|
@ -52,7 +51,7 @@ import java.util.List;
|
||||||
private volatile int channelCount;
|
private volatile int channelCount;
|
||||||
private volatile int sampleRate;
|
private volatile int sampleRate;
|
||||||
|
|
||||||
public FfmpegDecoder(
|
public FfmpegAudioDecoder(
|
||||||
int numInputBuffers,
|
int numInputBuffers,
|
||||||
int numOutputBuffers,
|
int numOutputBuffers,
|
||||||
int initialInputBufferSize,
|
int initialInputBufferSize,
|
||||||
|
|
@ -64,9 +63,7 @@ import java.util.List;
|
||||||
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
|
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
|
||||||
}
|
}
|
||||||
Assertions.checkNotNull(format.sampleMimeType);
|
Assertions.checkNotNull(format.sampleMimeType);
|
||||||
codecName =
|
codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType));
|
||||||
Assertions.checkNotNull(
|
|
||||||
FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));
|
|
||||||
extraData = getExtraData(format.sampleMimeType, format.initializationData);
|
extraData = getExtraData(format.sampleMimeType, format.initializationData);
|
||||||
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
|
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
|
||||||
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
|
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
|
||||||
|
|
@ -90,7 +87,7 @@ import java.util.List;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected SimpleOutputBuffer createOutputBuffer() {
|
protected SimpleOutputBuffer createOutputBuffer() {
|
||||||
return new SimpleOutputBuffer(this);
|
return new SimpleOutputBuffer(this::releaseOutputBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -111,13 +108,13 @@ import java.util.List;
|
||||||
int inputSize = inputData.limit();
|
int inputSize = inputData.limit();
|
||||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
||||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, 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
|
// 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
|
// 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.
|
// position is reset when more audio is produced.
|
||||||
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
|
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
|
||||||
return null;
|
return null;
|
||||||
} else if (result == DECODER_ERROR_OTHER) {
|
} else if (result == AUDIO_DECODER_ERROR_OTHER) {
|
||||||
return new FfmpegDecoderException("Error decoding (see logcat).");
|
return new FfmpegDecoderException("Error decoding (see logcat).");
|
||||||
}
|
}
|
||||||
if (!hasOutputFormat) {
|
if (!hasOutputFormat) {
|
||||||
|
|
@ -125,8 +122,8 @@ import java.util.List;
|
||||||
sampleRate = ffmpegGetSampleRate(nativeContext);
|
sampleRate = ffmpegGetSampleRate(nativeContext);
|
||||||
if (sampleRate == 0 && "alac".equals(codecName)) {
|
if (sampleRate == 0 && "alac".equals(codecName)) {
|
||||||
Assertions.checkNotNull(extraData);
|
Assertions.checkNotNull(extraData);
|
||||||
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
|
// ALAC decoder did not set the sample rate in earlier versions of FFmpeg. See
|
||||||
// See https://trac.ffmpeg.org/ticket/6096
|
// https://trac.ffmpeg.org/ticket/6096.
|
||||||
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
|
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
|
||||||
parsableExtraData.setPosition(extraData.length - 4);
|
parsableExtraData.setPosition(extraData.length - 4);
|
||||||
sampleRate = parsableExtraData.readUnsignedIntToInt();
|
sampleRate = parsableExtraData.readUnsignedIntToInt();
|
||||||
|
|
@ -145,23 +142,17 @@ import java.util.List;
|
||||||
nativeContext = 0;
|
nativeContext = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the channel count of output audio. */
|
||||||
* Returns the channel count of output audio. May only be called after {@link #decode}.
|
|
||||||
*/
|
|
||||||
public int getChannelCount() {
|
public int getChannelCount() {
|
||||||
return channelCount;
|
return channelCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the sample rate of output audio. */
|
||||||
* Returns the sample rate of output audio. May only be called after {@link #decode}.
|
|
||||||
*/
|
|
||||||
public int getSampleRate() {
|
public int getSampleRate() {
|
||||||
return sampleRate;
|
return sampleRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the encoding of output audio. */
|
||||||
* Returns the encoding of output audio.
|
|
||||||
*/
|
|
||||||
public @C.Encoding int getEncoding() {
|
public @C.Encoding int getEncoding() {
|
||||||
return encoding;
|
return encoding;
|
||||||
}
|
}
|
||||||
|
|
@ -223,13 +214,14 @@ import java.util.List;
|
||||||
int rawSampleRate,
|
int rawSampleRate,
|
||||||
int rawChannelCount);
|
int rawChannelCount);
|
||||||
|
|
||||||
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
|
private native int ffmpegDecode(
|
||||||
ByteBuffer outputData, int outputSize);
|
long context, ByteBuffer inputData, int inputSize, ByteBuffer outputData, int outputSize);
|
||||||
|
|
||||||
private native int ffmpegGetChannelCount(long context);
|
private native int ffmpegGetChannelCount(long context);
|
||||||
|
|
||||||
private native int ffmpegGetSampleRate(long context);
|
private native int ffmpegGetSampleRate(long context);
|
||||||
|
|
||||||
private native long ffmpegReset(long context, @Nullable byte[] extraData);
|
private native long ffmpegReset(long context, @Nullable byte[] extraData);
|
||||||
|
|
||||||
private native void ffmpegRelease(long context);
|
private native void ffmpegRelease(long context);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -18,24 +18,22 @@ package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||||
import com.google.android.exoplayer2.audio.AudioSink;
|
import com.google.android.exoplayer2.audio.AudioSink;
|
||||||
|
import com.google.android.exoplayer2.audio.DecoderAudioRenderer;
|
||||||
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
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.drm.ExoMediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import java.util.Collections;
|
import com.google.android.exoplayer2.util.TraceUtil;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/**
|
/** Decodes and renders audio using FFmpeg. */
|
||||||
* Decodes and renders audio using FFmpeg.
|
public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
|
||||||
*/
|
|
||||||
public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
private static final String TAG = "FfmpegAudioRenderer";
|
||||||
|
|
||||||
/** The number of input and output buffers. */
|
/** The number of input and output buffers. */
|
||||||
private static final int NUM_BUFFERS = 16;
|
private static final int NUM_BUFFERS = 16;
|
||||||
|
|
@ -44,13 +42,15 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
|
|
||||||
private final boolean enableFloatOutput;
|
private final boolean enableFloatOutput;
|
||||||
|
|
||||||
private @MonotonicNonNull FfmpegDecoder decoder;
|
private @MonotonicNonNull FfmpegAudioDecoder decoder;
|
||||||
|
|
||||||
public FfmpegAudioRenderer() {
|
public FfmpegAudioRenderer() {
|
||||||
this(/* eventHandler= */ null, /* eventListener= */ null);
|
this(/* eventHandler= */ null, /* eventListener= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
* null if delivery of events is not required.
|
* 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 eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
|
|
@ -68,6 +68,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
* null if delivery of events is not required.
|
* 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 eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
|
|
@ -85,22 +87,24 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
super(
|
super(
|
||||||
eventHandler,
|
eventHandler,
|
||||||
eventListener,
|
eventListener,
|
||||||
/* drmSessionManager= */ null,
|
|
||||||
/* playClearSamplesWithoutKeys= */ false,
|
|
||||||
audioSink);
|
audioSink);
|
||||||
this.enableFloatOutput = enableFloatOutput;
|
this.enableFloatOutput = enableFloatOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int supportsFormatInternal(
|
public String getName() {
|
||||||
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
|
return TAG;
|
||||||
Assertions.checkNotNull(format.sampleMimeType);
|
}
|
||||||
if (!FfmpegLibrary.isAvailable()) {
|
|
||||||
|
@Override
|
||||||
|
@FormatSupport
|
||||||
|
protected int supportsFormatInternal(Format format) {
|
||||||
|
String mimeType = Assertions.checkNotNull(format.sampleMimeType);
|
||||||
|
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) {
|
||||||
return FORMAT_UNSUPPORTED_TYPE;
|
return FORMAT_UNSUPPORTED_TYPE;
|
||||||
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding)
|
} else if (!FfmpegLibrary.supportsFormat(mimeType) || !isOutputSupported(format)) {
|
||||||
|| !isOutputSupported(format)) {
|
|
||||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) {
|
||||||
return FORMAT_UNSUPPORTED_DRM;
|
return FORMAT_UNSUPPORTED_DRM;
|
||||||
} else {
|
} else {
|
||||||
return FORMAT_HANDLED;
|
return FORMAT_HANDLED;
|
||||||
|
|
@ -108,40 +112,33 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
|
@AdaptiveSupport
|
||||||
|
public final int supportsMixedMimeTypeAdaptation() {
|
||||||
return ADAPTIVE_NOT_SEAMLESS;
|
return ADAPTIVE_NOT_SEAMLESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
protected FfmpegAudioDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||||
throws FfmpegDecoderException {
|
throws FfmpegDecoderException {
|
||||||
|
TraceUtil.beginSection("createFfmpegAudioDecoder");
|
||||||
int initialInputBufferSize =
|
int initialInputBufferSize =
|
||||||
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
|
||||||
decoder =
|
decoder =
|
||||||
new FfmpegDecoder(
|
new FfmpegAudioDecoder(
|
||||||
NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));
|
NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));
|
||||||
|
TraceUtil.endSection();
|
||||||
return decoder;
|
return decoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Format getOutputFormat() {
|
public Format getOutputFormat() {
|
||||||
Assertions.checkNotNull(decoder);
|
Assertions.checkNotNull(decoder);
|
||||||
int channelCount = decoder.getChannelCount();
|
return new Format.Builder()
|
||||||
int sampleRate = decoder.getSampleRate();
|
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||||
@C.PcmEncoding int encoding = decoder.getEncoding();
|
.setChannelCount(decoder.getChannelCount())
|
||||||
return Format.createAudioSampleFormat(
|
.setSampleRate(decoder.getSampleRate())
|
||||||
/* id= */ null,
|
.setPcmEncoding(decoder.getEncoding())
|
||||||
MimeTypes.AUDIO_RAW,
|
.build();
|
||||||
/* codecs= */ null,
|
|
||||||
Format.NO_VALUE,
|
|
||||||
Format.NO_VALUE,
|
|
||||||
channelCount,
|
|
||||||
sampleRate,
|
|
||||||
encoding,
|
|
||||||
Collections.emptyList(),
|
|
||||||
/* drmInitData= */ null,
|
|
||||||
/* selectionFlags= */ 0,
|
|
||||||
/* language= */ null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isOutputSupported(Format inputFormat) {
|
private boolean isOutputSupported(Format inputFormat) {
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
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. */
|
||||||
* Thrown when an FFmpeg decoder error occurs.
|
public final class FfmpegDecoderException extends DecoderException {
|
||||||
*/
|
|
||||||
public final class FfmpegDecoderException extends AudioDecoderException {
|
|
||||||
|
|
||||||
/* package */ FfmpegDecoderException(String message) {
|
/* package */ FfmpegDecoderException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
|
@ -34,14 +33,14 @@ public final class FfmpegLibrary {
|
||||||
private static final String TAG = "FfmpegLibrary";
|
private static final String TAG = "FfmpegLibrary";
|
||||||
|
|
||||||
private static final LibraryLoader LOADER =
|
private static final LibraryLoader LOADER =
|
||||||
new LibraryLoader("avutil", "avresample", "swresample", "avcodec", "ffmpeg");
|
new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg");
|
||||||
|
|
||||||
private FfmpegLibrary() {}
|
private FfmpegLibrary() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the names of the FFmpeg native libraries. If an application wishes to call this
|
* Override the names of the FFmpeg native libraries. If an application wishes to call this
|
||||||
* method, it must do so before calling any other method defined by this class, and before
|
* method, it must do so before calling any other method defined by this class, and before
|
||||||
* instantiating a {@link FfmpegAudioRenderer} instance.
|
* instantiating a {@link FfmpegAudioRenderer} or {@link FfmpegVideoRenderer} instance.
|
||||||
*
|
*
|
||||||
* @param libraries The names of the FFmpeg native libraries.
|
* @param libraries The names of the FFmpeg native libraries.
|
||||||
*/
|
*/
|
||||||
|
|
@ -57,7 +56,8 @@ public final class FfmpegLibrary {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the version of the underlying library if available, or null otherwise. */
|
/** Returns the version of the underlying library if available, or null otherwise. */
|
||||||
public static @Nullable String getVersion() {
|
@Nullable
|
||||||
|
public static String getVersion() {
|
||||||
return isAvailable() ? ffmpegGetVersion() : null;
|
return isAvailable() ? ffmpegGetVersion() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,13 +65,12 @@ public final class FfmpegLibrary {
|
||||||
* Returns whether the underlying library supports the specified MIME type.
|
* Returns whether the underlying library supports the specified MIME type.
|
||||||
*
|
*
|
||||||
* @param mimeType The MIME type to check.
|
* @param mimeType The MIME type to check.
|
||||||
* @param encoding The PCM encoding for raw audio.
|
|
||||||
*/
|
*/
|
||||||
public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) {
|
public static boolean supportsFormat(String mimeType) {
|
||||||
if (!isAvailable()) {
|
if (!isAvailable()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String codecName = getCodecName(mimeType, encoding);
|
@Nullable String codecName = getCodecName(mimeType);
|
||||||
if (codecName == null) {
|
if (codecName == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +85,8 @@ public final class FfmpegLibrary {
|
||||||
* Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
|
* Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
|
||||||
* if it's unsupported.
|
* if it's unsupported.
|
||||||
*/
|
*/
|
||||||
/* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) {
|
@Nullable
|
||||||
|
/* package */ static String getCodecName(String mimeType) {
|
||||||
switch (mimeType) {
|
switch (mimeType) {
|
||||||
case MimeTypes.AUDIO_AAC:
|
case MimeTypes.AUDIO_AAC:
|
||||||
return "aac";
|
return "aac";
|
||||||
|
|
@ -116,14 +116,14 @@ public final class FfmpegLibrary {
|
||||||
return "flac";
|
return "flac";
|
||||||
case MimeTypes.AUDIO_ALAC:
|
case MimeTypes.AUDIO_ALAC:
|
||||||
return "alac";
|
return "alac";
|
||||||
case MimeTypes.AUDIO_RAW:
|
case MimeTypes.AUDIO_MLAW:
|
||||||
if (encoding == C.ENCODING_PCM_MU_LAW) {
|
return "pcm_mulaw";
|
||||||
return "pcm_mulaw";
|
case MimeTypes.AUDIO_ALAW:
|
||||||
} else if (encoding == C.ENCODING_PCM_A_LAW) {
|
return "pcm_alaw";
|
||||||
return "pcm_alaw";
|
case MimeTypes.VIDEO_H264:
|
||||||
} else {
|
return "h264";
|
||||||
return null;
|
case MimeTypes.VIDEO_H265:
|
||||||
}
|
return "hevc";
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.Surface;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.RendererCapabilities;
|
||||||
|
import com.google.android.exoplayer2.decoder.Decoder;
|
||||||
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
|
import com.google.android.exoplayer2.util.TraceUtil;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import com.google.android.exoplayer2.video.DecoderVideoRenderer;
|
||||||
|
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
|
||||||
|
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
|
||||||
|
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||||
|
|
||||||
|
// TODO: Remove the NOTE below.
|
||||||
|
/**
|
||||||
|
* <b>NOTE: This class if under development and is not yet functional.</b>
|
||||||
|
*
|
||||||
|
* <p>Decodes and renders video using FFmpeg.
|
||||||
|
*/
|
||||||
|
public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
|
||||||
|
|
||||||
|
private static final String TAG = "FfmpegAudioRenderer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
|
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
|
||||||
|
* can attempt to seamlessly join an ongoing playback.
|
||||||
|
* @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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between
|
||||||
|
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
|
||||||
|
*/
|
||||||
|
public FfmpegVideoRenderer(
|
||||||
|
long allowedJoiningTimeMs,
|
||||||
|
@Nullable Handler eventHandler,
|
||||||
|
@Nullable VideoRendererEventListener eventListener,
|
||||||
|
int maxDroppedFramesToNotify) {
|
||||||
|
super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify);
|
||||||
|
// TODO: Implement.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@RendererCapabilities.Capabilities
|
||||||
|
public final int supportsFormat(Format format) {
|
||||||
|
// TODO: Remove this line and uncomment the implementation below.
|
||||||
|
return FORMAT_UNSUPPORTED_TYPE;
|
||||||
|
/*
|
||||||
|
String mimeType = Assertions.checkNotNull(format.sampleMimeType);
|
||||||
|
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isVideo(mimeType)) {
|
||||||
|
return FORMAT_UNSUPPORTED_TYPE;
|
||||||
|
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) {
|
||||||
|
return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);
|
||||||
|
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) {
|
||||||
|
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
|
||||||
|
} else {
|
||||||
|
return RendererCapabilities.create(
|
||||||
|
FORMAT_HANDLED,
|
||||||
|
ADAPTIVE_SEAMLESS,
|
||||||
|
TUNNELING_NOT_SUPPORTED);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("return.type.incompatible")
|
||||||
|
@Override
|
||||||
|
protected Decoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, FfmpegDecoderException>
|
||||||
|
createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||||
|
throws FfmpegDecoderException {
|
||||||
|
TraceUtil.beginSection("createFfmpegVideoDecoder");
|
||||||
|
// TODO: Implement, remove the SuppressWarnings annotation, and update the return type to use
|
||||||
|
// the concrete type of the decoder (probably FfmepgVideoDecoder).
|
||||||
|
TraceUtil.endSection();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
|
||||||
|
throws FfmpegDecoderException {
|
||||||
|
// TODO: Implement.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
|
||||||
|
// TODO: Uncomment the implementation below.
|
||||||
|
/*
|
||||||
|
if (decoder != null) {
|
||||||
|
decoder.setOutputMode(outputMode);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean canKeepCodec(Format oldFormat, Format newFormat) {
|
||||||
|
return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,11 +21,6 @@ LOCAL_MODULE := libavcodec
|
||||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||||
include $(PREBUILT_SHARED_LIBRARY)
|
include $(PREBUILT_SHARED_LIBRARY)
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
LOCAL_MODULE := libavresample
|
|
||||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
|
||||||
include $(PREBUILT_SHARED_LIBRARY)
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
include $(CLEAR_VARS)
|
||||||
LOCAL_MODULE := libswresample
|
LOCAL_MODULE := libswresample
|
||||||
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
|
||||||
|
|
@ -40,6 +35,6 @@ include $(CLEAR_VARS)
|
||||||
LOCAL_MODULE := ffmpeg
|
LOCAL_MODULE := ffmpeg
|
||||||
LOCAL_SRC_FILES := ffmpeg_jni.cc
|
LOCAL_SRC_FILES := ffmpeg_jni.cc
|
||||||
LOCAL_C_INCLUDES := ffmpeg
|
LOCAL_C_INCLUDES := ffmpeg
|
||||||
LOCAL_SHARED_LIBRARIES := libavcodec libavresample libswresample libavutil
|
LOCAL_SHARED_LIBRARIES := libavcodec libswresample libavutil
|
||||||
LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog
|
LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog
|
||||||
include $(BUILD_SHARED_LIBRARY)
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,9 @@ COMMON_OPTIONS="
|
||||||
--disable-postproc
|
--disable-postproc
|
||||||
--disable-avfilter
|
--disable-avfilter
|
||||||
--disable-symver
|
--disable-symver
|
||||||
--enable-avresample
|
--disable-avresample
|
||||||
--enable-swresample
|
--enable-swresample
|
||||||
|
--extra-ldexeflags=-pie
|
||||||
"
|
"
|
||||||
TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin"
|
TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin"
|
||||||
for decoder in "${ENABLED_DECODERS[@]}"
|
for decoder in "${ENABLED_DECODERS[@]}"
|
||||||
|
|
@ -53,7 +54,6 @@ git checkout release/4.2
|
||||||
--strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \
|
--strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \
|
||||||
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
|
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
|
||||||
--extra-ldflags="-Wl,--fix-cortex-a8" \
|
--extra-ldflags="-Wl,--fix-cortex-a8" \
|
||||||
--extra-ldexeflags=-pie \
|
|
||||||
${COMMON_OPTIONS}
|
${COMMON_OPTIONS}
|
||||||
make -j4
|
make -j4
|
||||||
make install-libs
|
make install-libs
|
||||||
|
|
@ -65,7 +65,6 @@ make clean
|
||||||
--cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \
|
--cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \
|
||||||
--nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \
|
--nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \
|
||||||
--strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \
|
--strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \
|
||||||
--extra-ldexeflags=-pie \
|
|
||||||
${COMMON_OPTIONS}
|
${COMMON_OPTIONS}
|
||||||
make -j4
|
make -j4
|
||||||
make install-libs
|
make install-libs
|
||||||
|
|
@ -77,7 +76,18 @@ make clean
|
||||||
--cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \
|
--cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \
|
||||||
--nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \
|
--nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \
|
||||||
--strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \
|
--strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \
|
||||||
--extra-ldexeflags=-pie \
|
--disable-asm \
|
||||||
|
${COMMON_OPTIONS}
|
||||||
|
make -j4
|
||||||
|
make install-libs
|
||||||
|
make clean
|
||||||
|
./configure \
|
||||||
|
--libdir=android-libs/x86_64 \
|
||||||
|
--arch=x86_64 \
|
||||||
|
--cpu=x86_64 \
|
||||||
|
--cross-prefix="${TOOLCHAIN_PREFIX}/x86_64-linux-android21-" \
|
||||||
|
--nm="${TOOLCHAIN_PREFIX}/x86_64-linux-android-nm" \
|
||||||
|
--strip="${TOOLCHAIN_PREFIX}/x86_64-linux-android-strip" \
|
||||||
--disable-asm \
|
--disable-asm \
|
||||||
${COMMON_OPTIONS}
|
${COMMON_OPTIONS}
|
||||||
make -j4
|
make -j4
|
||||||
|
|
|
||||||
|
|
@ -26,35 +26,35 @@ extern "C" {
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#endif
|
#endif
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavresample/avresample.h>
|
|
||||||
#include <libavutil/channel_layout.h>
|
#include <libavutil/channel_layout.h>
|
||||||
#include <libavutil/error.h>
|
#include <libavutil/error.h>
|
||||||
#include <libavutil/opt.h>
|
#include <libavutil/opt.h>
|
||||||
|
#include <libswresample/swresample.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
#define LOG_TAG "ffmpeg_jni"
|
#define LOG_TAG "ffmpeg_jni"
|
||||||
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \
|
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, \
|
||||||
__VA_ARGS__))
|
__VA_ARGS__))
|
||||||
|
|
||||||
#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \
|
#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \
|
||||||
extern "C" { \
|
extern "C" { \
|
||||||
JNIEXPORT RETURN_TYPE \
|
JNIEXPORT RETURN_TYPE \
|
||||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegDecoder_ ## NAME \
|
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_##NAME( \
|
||||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__);\
|
JNIEnv *env, jobject thiz, ##__VA_ARGS__); \
|
||||||
} \
|
} \
|
||||||
JNIEXPORT RETURN_TYPE \
|
JNIEXPORT RETURN_TYPE \
|
||||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegDecoder_ ## NAME \
|
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_##NAME( \
|
||||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
|
JNIEnv *env, jobject thiz, ##__VA_ARGS__)
|
||||||
|
|
||||||
#define LIBRARY_FUNC(RETURN_TYPE, NAME, ...) \
|
#define AUDIO_DECODER_FUNC(RETURN_TYPE, NAME, ...) \
|
||||||
extern "C" { \
|
extern "C" { \
|
||||||
JNIEXPORT RETURN_TYPE \
|
JNIEXPORT RETURN_TYPE \
|
||||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_ ## NAME \
|
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegAudioDecoder_##NAME( \
|
||||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__);\
|
JNIEnv *env, jobject thiz, ##__VA_ARGS__); \
|
||||||
} \
|
} \
|
||||||
JNIEXPORT RETURN_TYPE \
|
JNIEXPORT RETURN_TYPE \
|
||||||
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegLibrary_ ## NAME \
|
Java_com_google_android_exoplayer2_ext_ffmpeg_FfmpegAudioDecoder_##NAME( \
|
||||||
(JNIEnv* env, jobject thiz, ##__VA_ARGS__)\
|
JNIEnv *env, jobject thiz, ##__VA_ARGS__)
|
||||||
|
|
||||||
#define ERROR_STRING_BUFFER_LENGTH 256
|
#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.
|
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
|
||||||
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
|
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
|
||||||
|
|
||||||
// Error codes matching FfmpegDecoder.java.
|
// LINT.IfChange
|
||||||
static const int DECODER_ERROR_INVALID_DATA = -1;
|
static const int AUDIO_DECODER_ERROR_INVALID_DATA = -1;
|
||||||
static const int DECODER_ERROR_OTHER = -2;
|
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.
|
* 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
|
* 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,
|
int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||||
uint8_t *outputBuffer, int outputSize);
|
uint8_t *outputBuffer, int outputSize);
|
||||||
|
|
@ -115,8 +117,9 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
|
||||||
return getCodecByName(env, codecName) != NULL;
|
return getCodecByName(env, codecName) != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
|
AUDIO_DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName,
|
||||||
jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) {
|
jbyteArray extraData, jboolean outputFloat,
|
||||||
|
jint rawSampleRate, jint rawChannelCount) {
|
||||||
AVCodec *codec = getCodecByName(env, codecName);
|
AVCodec *codec = getCodecByName(env, codecName);
|
||||||
if (!codec) {
|
if (!codec) {
|
||||||
LOGE("Codec not found.");
|
LOGE("Codec not found.");
|
||||||
|
|
@ -126,8 +129,8 @@ DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
|
||||||
rawChannelCount);
|
rawChannelCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
AUDIO_DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
||||||
jint inputSize, jobject outputData, jint outputSize) {
|
jint inputSize, jobject outputData, jint outputSize) {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
LOGE("Context must be non-NULL.");
|
LOGE("Context must be non-NULL.");
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -154,7 +157,7 @@ DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
|
||||||
outputSize);
|
outputSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) {
|
AUDIO_DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
LOGE("Context must be non-NULL.");
|
LOGE("Context must be non-NULL.");
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -162,7 +165,7 @@ DECODER_FUNC(jint, ffmpegGetChannelCount, jlong context) {
|
||||||
return ((AVCodecContext *) context)->channels;
|
return ((AVCodecContext *) context)->channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) {
|
AUDIO_DECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
LOGE("Context must be non-NULL.");
|
LOGE("Context must be non-NULL.");
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -170,7 +173,7 @@ DECODER_FUNC(jint, ffmpegGetSampleRate, jlong context) {
|
||||||
return ((AVCodecContext *) context)->sample_rate;
|
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;
|
AVCodecContext *context = (AVCodecContext *) jContext;
|
||||||
if (!context) {
|
if (!context) {
|
||||||
LOGE("Tried to reset without a context.");
|
LOGE("Tried to reset without a context.");
|
||||||
|
|
@ -198,7 +201,7 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
|
||||||
return (jlong) context;
|
return (jlong) context;
|
||||||
}
|
}
|
||||||
|
|
||||||
DECODER_FUNC(void, ffmpegRelease, jlong context) {
|
AUDIO_DECODER_FUNC(void, ffmpegRelease, jlong context) {
|
||||||
if (context) {
|
if (context) {
|
||||||
releaseContext((AVCodecContext *) context);
|
releaseContext((AVCodecContext *) context);
|
||||||
}
|
}
|
||||||
|
|
@ -259,8 +262,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||||
result = avcodec_send_packet(context, packet);
|
result = avcodec_send_packet(context, packet);
|
||||||
if (result) {
|
if (result) {
|
||||||
logError("avcodec_send_packet", result);
|
logError("avcodec_send_packet", result);
|
||||||
return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA
|
return result == AVERROR_INVALIDDATA ? AUDIO_DECODER_ERROR_INVALID_DATA
|
||||||
: DECODER_ERROR_OTHER;
|
: AUDIO_DECODER_ERROR_OTHER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dequeue output data until it runs out.
|
// Dequeue output data until it runs out.
|
||||||
|
|
@ -289,11 +292,11 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||||
int sampleCount = frame->nb_samples;
|
int sampleCount = frame->nb_samples;
|
||||||
int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount,
|
int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount,
|
||||||
sampleFormat, 1);
|
sampleFormat, 1);
|
||||||
AVAudioResampleContext *resampleContext;
|
SwrContext *resampleContext;
|
||||||
if (context->opaque) {
|
if (context->opaque) {
|
||||||
resampleContext = (AVAudioResampleContext *) context->opaque;
|
resampleContext = (SwrContext *)context->opaque;
|
||||||
} else {
|
} else {
|
||||||
resampleContext = avresample_alloc_context();
|
resampleContext = swr_alloc();
|
||||||
av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0);
|
av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0);
|
||||||
av_opt_set_int(resampleContext, "out_channel_layout", channelLayout, 0);
|
av_opt_set_int(resampleContext, "out_channel_layout", channelLayout, 0);
|
||||||
av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
|
av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
|
||||||
|
|
@ -302,9 +305,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||||
// The output format is always the requested format.
|
// The output format is always the requested format.
|
||||||
av_opt_set_int(resampleContext, "out_sample_fmt",
|
av_opt_set_int(resampleContext, "out_sample_fmt",
|
||||||
context->request_sample_fmt, 0);
|
context->request_sample_fmt, 0);
|
||||||
result = avresample_open(resampleContext);
|
result = swr_init(resampleContext);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
logError("avresample_open", result);
|
logError("swr_init", result);
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
@ -312,7 +315,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||||
}
|
}
|
||||||
int inSampleSize = av_get_bytes_per_sample(sampleFormat);
|
int inSampleSize = av_get_bytes_per_sample(sampleFormat);
|
||||||
int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
|
int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
|
||||||
int outSamples = avresample_get_out_samples(resampleContext, sampleCount);
|
int outSamples = swr_get_out_samples(resampleContext, sampleCount);
|
||||||
int bufferOutSize = outSampleSize * channelCount * outSamples;
|
int bufferOutSize = outSampleSize * channelCount * outSamples;
|
||||||
if (outSize + bufferOutSize > outputSize) {
|
if (outSize + bufferOutSize > outputSize) {
|
||||||
LOGE("Output buffer size (%d) too small for output data (%d).",
|
LOGE("Output buffer size (%d) too small for output data (%d).",
|
||||||
|
|
@ -320,15 +323,14 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
result = avresample_convert(resampleContext, &outputBuffer, bufferOutSize,
|
result = swr_convert(resampleContext, &outputBuffer, bufferOutSize,
|
||||||
outSamples, frame->data, frame->linesize[0],
|
(const uint8_t **)frame->data, frame->nb_samples);
|
||||||
sampleCount);
|
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
logError("avresample_convert", result);
|
logError("swr_convert", result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
int available = avresample_available(resampleContext);
|
int available = swr_get_out_samples(resampleContext, 0);
|
||||||
if (available != 0) {
|
if (available != 0) {
|
||||||
LOGE("Expected no samples remaining after resampling, but found %d.",
|
LOGE("Expected no samples remaining after resampling, but found %d.",
|
||||||
available);
|
available);
|
||||||
|
|
@ -351,9 +353,9 @@ void releaseContext(AVCodecContext *context) {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AVAudioResampleContext *resampleContext;
|
SwrContext *swrContext;
|
||||||
if ((resampleContext = (AVAudioResampleContext *) context->opaque)) {
|
if ((swrContext = (SwrContext *)context->opaque)) {
|
||||||
avresample_free(&resampleContext);
|
swr_free(&swrContext);
|
||||||
context->opaque = NULL;
|
context->opaque = NULL;
|
||||||
}
|
}
|
||||||
avcodec_free_context(&context);
|
avcodec_free_context(&context);
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,14 @@ a custom track selector the choice of `Renderer` is up to your implementation,
|
||||||
so you need to make sure you are passing an `LibflacAudioRenderer` to the
|
so you need to make sure you are passing an `LibflacAudioRenderer` to the
|
||||||
player, then implement your own logic to use the renderer for a given track.
|
player, then implement your own logic to use the renderer for a given track.
|
||||||
|
|
||||||
|
## Using the extension in the demo application ##
|
||||||
|
|
||||||
|
To try out playback using the extension in the [demo application][], see
|
||||||
|
[enabling extension decoders][].
|
||||||
|
|
||||||
|
[demo application]: https://exoplayer.dev/demo-application.html
|
||||||
|
[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
|
||||||
|
|
||||||
## Links ##
|
## Links ##
|
||||||
|
|
||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,12 @@ android {
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.main {
|
sourceSets {
|
||||||
jniLibs.srcDir 'src/main/libs'
|
main {
|
||||||
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
jniLibs.srcDir 'src/main/libs'
|
||||||
|
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
||||||
|
}
|
||||||
|
androidTest.assets.srcDir '../../testdata/src/test/assets/'
|
||||||
}
|
}
|
||||||
|
|
||||||
testOptions.unitTests.includeAndroidResources = true
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
|
|
@ -41,11 +44,13 @@ dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
androidTestImplementation project(modulePrefix + 'testutils')
|
androidTestImplementation project(modulePrefix + 'testutils')
|
||||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||||
testImplementation 'androidx.test:core:' + androidxTestCoreVersion
|
testImplementation 'androidx.test:core:' + androidxTestCoreVersion
|
||||||
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
|
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
|
testImplementation project(modulePrefix + 'testdata')
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
|
-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 {
|
-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,7 @@
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"/>
|
||||||
<uses-library android:name="android.test.runner"/>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
<instrumentation
|
<instrumentation
|
||||||
android:targetPackage="com.google.android.exoplayer2.ext.flac.test"
|
android:targetPackage="com.google.android.exoplayer2.ext.flac.test"
|
||||||
|
|
|
||||||
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;
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
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.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
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.DefaultDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
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)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class FlacExtractorSeekTest {
|
public final class FlacExtractorSeekTest {
|
||||||
|
|
||||||
private static final String NO_SEEKTABLE_FLAC = "bear_no_seek.flac";
|
private static final String TEST_FILE_SEEK_TABLE = "flac/bear.flac";
|
||||||
|
private static final String TEST_FILE_BINARY_SEARCH = "flac/bear_one_metadata_block.flac";
|
||||||
|
private static final String TEST_FILE_UNSEEKABLE = "flac/bear_no_seek_table_no_num_samples.flac";
|
||||||
private static final int DURATION_US = 2_741_000;
|
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 FlacExtractor extractor = new FlacExtractor();
|
||||||
private FakeTrackOutput expectedTrackOutput;
|
private FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
||||||
|
private DefaultDataSource dataSource =
|
||||||
private DefaultDataSource dataSource;
|
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent")
|
||||||
private PositionHolder positionHolder;
|
.createDataSource();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap()
|
public void flacExtractorReads_seekTable_returnSeekableSeekMap() throws IOException {
|
||||||
throws IOException, InterruptedException {
|
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_SEEK_TABLE);
|
||||||
FlacExtractor extractor = new FlacExtractor();
|
|
||||||
|
|
||||||
SeekMap seekMap = extractSeekMap(extractor, new FakeExtractorOutput());
|
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||||
|
|
||||||
assertThat(seekMap).isNotNull();
|
assertThat(seekMap).isNotNull();
|
||||||
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
|
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
|
||||||
|
|
@ -90,205 +60,227 @@ public final class FlacExtractorSeekTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()
|
public void seeking_seekTable_handlesSeekToZero() throws IOException {
|
||||||
throws IOException, InterruptedException {
|
String fileName = TEST_FILE_SEEK_TABLE;
|
||||||
FlacExtractor extractor = new FlacExtractor();
|
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||||
|
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
|
||||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
|
||||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||||
|
|
||||||
long targetSeekTimeUs = 987_000;
|
long targetSeekTimeUs = 0;
|
||||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
int extractedFrameIndex =
|
||||||
|
TestUtil.seekToTimeUs(
|
||||||
|
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||||
|
|
||||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
assertFirstFrameAfterSeekPrecedesTargetSeekTime(
|
||||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()
|
public void seeking_seekTable_handlesSeekToEoF() throws IOException {
|
||||||
throws IOException, InterruptedException {
|
String fileName = TEST_FILE_SEEK_TABLE;
|
||||||
FlacExtractor extractor = new FlacExtractor();
|
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||||
|
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
|
||||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
|
||||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||||
|
|
||||||
long targetSeekTimeUs = seekMap.getDurationUs();
|
long targetSeekTimeUs = seekMap.getDurationUs();
|
||||||
|
int extractedFrameIndex =
|
||||||
|
TestUtil.seekToTimeUs(
|
||||||
|
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||||
|
|
||||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||||
|
assertFirstFrameAfterSeekPrecedesTargetSeekTime(
|
||||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
|
||||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()
|
public void seeking_seekTable_handlesSeekingBackward() throws IOException {
|
||||||
throws IOException, InterruptedException {
|
String fileName = TEST_FILE_SEEK_TABLE;
|
||||||
FlacExtractor extractor = new FlacExtractor();
|
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||||
|
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||||
|
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||||
|
|
||||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
long firstSeekTimeUs = 1_234_000;
|
||||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
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);
|
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||||
|
|
||||||
long firstSeekTimeUs = 987_000;
|
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;
|
long targetSeekTimeUs = 0;
|
||||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
int extractedFrameIndex =
|
||||||
|
TestUtil.seekToTimeUs(
|
||||||
|
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||||
|
|
||||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()
|
public void seeking_binarySearch_handlesSeekToEoF() throws IOException {
|
||||||
throws IOException, InterruptedException {
|
String fileName = TEST_FILE_BINARY_SEARCH;
|
||||||
FlacExtractor extractor = new FlacExtractor();
|
Uri fileUri = TestUtil.buildAssetUri(fileName);
|
||||||
|
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||||
|
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||||
|
|
||||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
long targetSeekTimeUs = seekMap.getDurationUs();
|
||||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
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);
|
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
||||||
|
|
||||||
long firstSeekTimeUs = 987_000;
|
long firstSeekTimeUs = 987_000;
|
||||||
seekToTimeUs(extractor, seekMap, firstSeekTimeUs, trackOutput);
|
TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);
|
||||||
|
|
||||||
long targetSeekTimeUs = 1_234_000;
|
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);
|
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
|
||||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
assertFirstFrameAfterSeekContainsTargetSeekTime(
|
||||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
fileName, trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()
|
public void flacExtractorReads_unseekable_returnUnseekableSeekMap() throws IOException {
|
||||||
throws IOException, InterruptedException {
|
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_UNSEEKABLE);
|
||||||
FlacExtractor extractor = new FlacExtractor();
|
|
||||||
|
|
||||||
FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
|
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
|
||||||
SeekMap seekMap = extractSeekMap(extractor, extractorOutput);
|
|
||||||
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
|
|
||||||
|
|
||||||
long numSeek = 100;
|
assertThat(seekMap).isNotNull();
|
||||||
for (long i = 0; i < numSeek; i++) {
|
assertThat(seekMap.getDurationUs()).isEqualTo(C.TIME_UNSET);
|
||||||
long targetSeekTimeUs = RANDOM.nextInt(DURATION_US + 1);
|
assertThat(seekMap.isSeekable()).isFalse();
|
||||||
int extractedFrameIndex = seekToTimeUs(extractor, seekMap, targetSeekTimeUs, trackOutput);
|
|
||||||
|
|
||||||
assertThat(extractedFrameIndex).isNotEqualTo(-1);
|
|
||||||
assertFirstFrameAfterSeekContainTargetSeekTime(
|
|
||||||
trackOutput, targetSeekTimeUs, extractedFrameIndex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(
|
trackOutput.assertSample(
|
||||||
firstFrameIndexAfterSeek,
|
firstFrameIndexAfterSeek,
|
||||||
expectedTrackOutput.getSampleData(expectedSampleIndex),
|
expectedTrackOutput.getSampleData(expectedFrameIndex),
|
||||||
expectedTrackOutput.getSampleTimeUs(expectedSampleIndex),
|
expectedTrackOutput.getSampleTimeUs(expectedFrameIndex),
|
||||||
expectedTrackOutput.getSampleFlags(expectedSampleIndex),
|
expectedTrackOutput.getSampleFlags(expectedFrameIndex),
|
||||||
expectedTrackOutput.getSampleCryptoData(expectedSampleIndex));
|
expectedTrackOutput.getSampleCryptoData(expectedFrameIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int findTargetFrameInExpectedOutput(long seekTimeUs) {
|
private static void assertFirstFrameAfterSeekPrecedesTargetSeekTime(
|
||||||
List<Long> sampleTimes = expectedTrackOutput.getSampleTimesUs();
|
String fileName,
|
||||||
for (int i = 0; i < sampleTimes.size() - 1; i++) {
|
FakeTrackOutput trackOutput,
|
||||||
long currentSampleTime = sampleTimes.get(i);
|
long targetSeekTimeUs,
|
||||||
long nextSampleTime = sampleTimes.get(i + 1);
|
int firstFrameIndexAfterSeek)
|
||||||
if (currentSampleTime <= seekTimeUs && nextSampleTime > seekTimeUs) {
|
throws IOException {
|
||||||
return i;
|
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 {
|
private static FakeTrackOutput getExpectedTrackOutput(String fileName) throws IOException {
|
||||||
DataSpec dataSpec = new DataSpec(FILE_URI, position, totalInputLength, /* key= */ null);
|
return TestUtil.extractAllSamplesFromFile(
|
||||||
dataSource.open(dataSpec);
|
new FlacExtractor(), ApplicationProvider.getApplicationContext(), fileName)
|
||||||
return new DefaultExtractorInput(dataSource, position, totalInputLength);
|
.trackOutputs
|
||||||
|
.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void extractAllSamplesFromFileToExpectedOutput(Context context, String fileName)
|
private static int getFrameIndex(FakeTrackOutput expectedTrackOutput, long targetSeekTimeUs) {
|
||||||
throws IOException, InterruptedException {
|
List<Long> frameTimes = expectedTrackOutput.getSampleTimesUs();
|
||||||
byte[] data = TestUtil.getByteArray(context, fileName);
|
return Util.binarySearchFloor(
|
||||||
|
frameTimes, targetSeekTimeUs, /* inclusive= */ true, /* stayInBounds= */ false);
|
||||||
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) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
|
@ -25,6 +24,8 @@ import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** Unit test for {@link FlacExtractor}. */
|
/** 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)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class FlacExtractorTest {
|
public class FlacExtractorTest {
|
||||||
|
|
||||||
|
|
@ -36,14 +37,80 @@ public class FlacExtractorTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExtractFlacSample() throws Exception {
|
public void sample() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
|
FlacExtractor::new, /* file= */ "flac/bear.flac", /* dumpFilesPrefix= */ "flac/bear_raw");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExtractFlacSampleWithId3Header() throws Exception {
|
public void sampleWithId3HeaderAndId3Enabled() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext());
|
FlacExtractor::new,
|
||||||
|
/* file= */ "flac/bear_with_id3.flac",
|
||||||
|
/* dumpFilesPrefix= */ "flac/bear_with_id3_enabled_raw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sampleWithId3HeaderAndId3Disabled() throws Exception {
|
||||||
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
|
() -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA),
|
||||||
|
/* file= */ "flac/bear_with_id3.flac",
|
||||||
|
/* dumpFilesPrefix= */ "flac/bear_with_id3_disabled_raw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sampleUnseekable() throws Exception {
|
||||||
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
|
FlacExtractor::new,
|
||||||
|
/* file= */ "flac/bear_no_seek_table_no_num_samples.flac",
|
||||||
|
/* dumpFilesPrefix= */ "flac/bear_no_seek_table_no_num_samples_raw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sampleWithVorbisComments() throws Exception {
|
||||||
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
|
FlacExtractor::new,
|
||||||
|
/* file= */ "flac/bear_with_vorbis_comments.flac",
|
||||||
|
/* dumpFilesPrefix= */ "flac/bear_with_vorbis_comments_raw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sampleWithPicture() throws Exception {
|
||||||
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
|
FlacExtractor::new,
|
||||||
|
/* file= */ "flac/bear_with_picture.flac",
|
||||||
|
/* dumpFilesPrefix= */ "flac/bear_with_picture_raw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void oneMetadataBlock() throws Exception {
|
||||||
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
|
FlacExtractor::new,
|
||||||
|
/* file= */ "flac/bear_one_metadata_block.flac",
|
||||||
|
/* dumpFilesPrefix= */ "flac/bear_one_metadata_block_raw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noMinMaxFrameSize() throws Exception {
|
||||||
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
|
FlacExtractor::new,
|
||||||
|
/* file= */ "flac/bear_no_min_max_frame_size.flac",
|
||||||
|
/* dumpFilesPrefix= */ "flac/bear_no_min_max_frame_size_raw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noNumSamples() throws Exception {
|
||||||
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
|
FlacExtractor::new,
|
||||||
|
/* file= */ "flac/bear_no_num_samples.flac",
|
||||||
|
/* dumpFilesPrefix= */ "flac/bear_no_num_samples_raw");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void uncommonSampleRate() throws Exception {
|
||||||
|
ExtractorAsserts.assertAllBehaviors(
|
||||||
|
FlacExtractor::new,
|
||||||
|
/* file= */ "flac/bear_uncommon_sample_rate.flac",
|
||||||
|
/* dumpFilesPrefix= */ "flac/bear_uncommon_sample_rate_raw");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,19 @@ import static org.junit.Assert.fail;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||||
|
import com.google.android.exoplayer2.audio.AudioSink;
|
||||||
|
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.testutil.CapturingAudioSink;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -37,7 +42,8 @@ import org.junit.runner.RunWith;
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class FlacPlaybackTest {
|
public class FlacPlaybackTest {
|
||||||
|
|
||||||
private static final String BEAR_FLAC_URI = "asset:///bear-flac.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
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
|
|
@ -47,38 +53,56 @@ public class FlacPlaybackTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBasicPlayback() throws Exception {
|
public void test16BitPlayback() throws Exception {
|
||||||
playUri(BEAR_FLAC_URI);
|
playAndAssertAudioSinkInput(BEAR_FLAC_16BIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void playUri(String uri) throws Exception {
|
@Test
|
||||||
|
public void test24BitPlayback() throws Exception {
|
||||||
|
playAndAssertAudioSinkInput(BEAR_FLAC_24BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void playAndAssertAudioSinkInput(String fileName) throws Exception {
|
||||||
|
CapturingAudioSink audioSink =
|
||||||
|
new CapturingAudioSink(
|
||||||
|
new DefaultAudioSink(/* audioCapabilities= */ null, new AudioProcessor[0]));
|
||||||
|
|
||||||
TestPlaybackRunnable testPlaybackRunnable =
|
TestPlaybackRunnable testPlaybackRunnable =
|
||||||
new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
|
new TestPlaybackRunnable(
|
||||||
|
Uri.parse("asset:///" + fileName),
|
||||||
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
audioSink);
|
||||||
Thread thread = new Thread(testPlaybackRunnable);
|
Thread thread = new Thread(testPlaybackRunnable);
|
||||||
thread.start();
|
thread.start();
|
||||||
thread.join();
|
thread.join();
|
||||||
if (testPlaybackRunnable.playbackException != null) {
|
if (testPlaybackRunnable.playbackException != null) {
|
||||||
throw testPlaybackRunnable.playbackException;
|
throw testPlaybackRunnable.playbackException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audioSink.assertOutput(
|
||||||
|
ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Uri uri;
|
private final Uri uri;
|
||||||
|
private final AudioSink audioSink;
|
||||||
|
|
||||||
private ExoPlayer player;
|
@Nullable private ExoPlayer player;
|
||||||
private ExoPlaybackException playbackException;
|
@Nullable private ExoPlaybackException playbackException;
|
||||||
|
|
||||||
public TestPlaybackRunnable(Uri uri, Context context) {
|
public TestPlaybackRunnable(Uri uri, Context context, AudioSink audioSink) {
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.audioSink = audioSink;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Looper.prepare();
|
Looper.prepare();
|
||||||
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
|
LibflacAudioRenderer audioRenderer =
|
||||||
|
new LibflacAudioRenderer(/* eventHandler= */ null, /* eventListener= */ null, audioSink);
|
||||||
player = new ExoPlayer.Builder(context, audioRenderer).build();
|
player = new ExoPlayer.Builder(context, audioRenderer).build();
|
||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
|
|
@ -86,8 +110,9 @@ public class FlacPlaybackTest {
|
||||||
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
|
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
|
||||||
MatroskaExtractor.FACTORY)
|
MatroskaExtractor.FACTORY)
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
player.prepare(mediaSource);
|
player.setMediaSource(mediaSource);
|
||||||
player.setPlayWhenReady(true);
|
player.prepare();
|
||||||
|
player.play();
|
||||||
Looper.loop();
|
Looper.loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,7 +122,7 @@ public class FlacPlaybackTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||||
if (playbackState == Player.STATE_ENDED
|
if (playbackState == Player.STATE_ENDED
|
||||||
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
|
|| (playbackState == Player.STATE_IDLE && playbackException != null)) {
|
||||||
player.release();
|
player.release();
|
||||||
|
|
@ -105,5 +130,4 @@ public class FlacPlaybackTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,10 @@ package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
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.SeekMap;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.FlacStreamMetadata;
|
import com.google.android.exoplayer2.util.FlacConstants;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
|
@ -49,6 +50,15 @@ import java.nio.ByteBuffer;
|
||||||
|
|
||||||
private final FlacDecoderJni decoderJni;
|
private final FlacDecoderJni decoderJni;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link FlacBinarySearchSeeker}.
|
||||||
|
*
|
||||||
|
* @param streamMetadata The stream metadata.
|
||||||
|
* @param firstFramePosition The byte offset of the first frame in the stream.
|
||||||
|
* @param inputLength The length of the stream in bytes.
|
||||||
|
* @param decoderJni The FLAC JNI decoder.
|
||||||
|
* @param outputFrameHolder A holder used to retrieve the frame found by a seeking operation.
|
||||||
|
*/
|
||||||
public FlacBinarySearchSeeker(
|
public FlacBinarySearchSeeker(
|
||||||
FlacStreamMetadata streamMetadata,
|
FlacStreamMetadata streamMetadata,
|
||||||
long firstFramePosition,
|
long firstFramePosition,
|
||||||
|
|
@ -56,7 +66,7 @@ import java.nio.ByteBuffer;
|
||||||
FlacDecoderJni decoderJni,
|
FlacDecoderJni decoderJni,
|
||||||
OutputFrameHolder outputFrameHolder) {
|
OutputFrameHolder outputFrameHolder) {
|
||||||
super(
|
super(
|
||||||
new FlacSeekTimestampConverter(streamMetadata),
|
/* seekTimestampConverter= */ streamMetadata::getSampleNumber,
|
||||||
new FlacTimestampSeeker(decoderJni, outputFrameHolder),
|
new FlacTimestampSeeker(decoderJni, outputFrameHolder),
|
||||||
streamMetadata.getDurationUs(),
|
streamMetadata.getDurationUs(),
|
||||||
/* floorTimePosition= */ 0,
|
/* floorTimePosition= */ 0,
|
||||||
|
|
@ -64,7 +74,8 @@ import java.nio.ByteBuffer;
|
||||||
/* floorBytePosition= */ firstFramePosition,
|
/* floorBytePosition= */ firstFramePosition,
|
||||||
/* ceilingBytePosition= */ inputLength,
|
/* ceilingBytePosition= */ inputLength,
|
||||||
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
|
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
|
||||||
/* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
|
/* minimumSearchRange= */ Math.max(
|
||||||
|
FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize));
|
||||||
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
this.decoderJni = Assertions.checkNotNull(decoderJni);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +100,7 @@ import java.nio.ByteBuffer;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleIndex)
|
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleIndex)
|
||||||
throws IOException, InterruptedException {
|
throws IOException {
|
||||||
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
|
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
|
||||||
long searchPosition = input.getPosition();
|
long searchPosition = input.getPosition();
|
||||||
decoderJni.reset(searchPosition);
|
decoderJni.reset(searchPosition);
|
||||||
|
|
@ -115,6 +126,8 @@ import java.nio.ByteBuffer;
|
||||||
if (targetSampleInLastFrame) {
|
if (targetSampleInLastFrame) {
|
||||||
// We are holding the target frame in outputFrameHolder. Set its presentation time now.
|
// We are holding the target frame in outputFrameHolder. Set its presentation time now.
|
||||||
outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();
|
outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();
|
||||||
|
// The input position is passed even though it does not indicate the frame containing the
|
||||||
|
// target sample because the extractor must continue to read from this position.
|
||||||
return TimestampSearchResult.targetFoundResult(input.getPosition());
|
return TimestampSearchResult.targetFoundResult(input.getPosition());
|
||||||
} else if (nextFrameSampleIndex <= targetSampleIndex) {
|
} else if (nextFrameSampleIndex <= targetSampleIndex) {
|
||||||
return TimestampSearchResult.underestimatedResult(
|
return TimestampSearchResult.underestimatedResult(
|
||||||
|
|
@ -124,21 +137,4 @@ import java.nio.ByteBuffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as
|
|
||||||
* the timestamp for a stream seek time position.
|
|
||||||
*/
|
|
||||||
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
|
|
||||||
private final FlacStreamMetadata streamMetadata;
|
|
||||||
|
|
||||||
public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
|
|
||||||
this.streamMetadata = streamMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long timeUsToTargetTime(long timeUs) {
|
|
||||||
return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import com.google.android.exoplayer2.ParserException;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
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 com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
@ -33,7 +33,7 @@ import java.util.List;
|
||||||
/* package */ final class FlacDecoder extends
|
/* package */ final class FlacDecoder extends
|
||||||
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FlacDecoderException> {
|
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FlacDecoderException> {
|
||||||
|
|
||||||
private final int maxOutputBufferSize;
|
private final FlacStreamMetadata streamMetadata;
|
||||||
private final FlacDecoderJni decoderJni;
|
private final FlacDecoderJni decoderJni;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -59,12 +59,11 @@ import java.util.List;
|
||||||
}
|
}
|
||||||
decoderJni = new FlacDecoderJni();
|
decoderJni = new FlacDecoderJni();
|
||||||
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
|
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
|
||||||
FlacStreamMetadata streamMetadata;
|
|
||||||
try {
|
try {
|
||||||
streamMetadata = decoderJni.decodeStreamMetadata();
|
streamMetadata = decoderJni.decodeStreamMetadata();
|
||||||
} catch (ParserException e) {
|
} catch (ParserException e) {
|
||||||
throw new FlacDecoderException("Failed to decode StreamInfo", e);
|
throw new FlacDecoderException("Failed to decode StreamInfo", e);
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException e) {
|
||||||
// Never happens.
|
// Never happens.
|
||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +71,6 @@ import java.util.List;
|
||||||
int initialInputBufferSize =
|
int initialInputBufferSize =
|
||||||
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
|
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
|
||||||
setInitialInputBufferSize(initialInputBufferSize);
|
setInitialInputBufferSize(initialInputBufferSize);
|
||||||
maxOutputBufferSize = streamMetadata.getMaxDecodedFrameSize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -87,7 +85,7 @@ import java.util.List;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected SimpleOutputBuffer createOutputBuffer() {
|
protected SimpleOutputBuffer createOutputBuffer() {
|
||||||
return new SimpleOutputBuffer(this);
|
return new SimpleOutputBuffer(this::releaseOutputBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -103,12 +101,13 @@ import java.util.List;
|
||||||
decoderJni.flush();
|
decoderJni.flush();
|
||||||
}
|
}
|
||||||
decoderJni.setData(Util.castNonNull(inputBuffer.data));
|
decoderJni.setData(Util.castNonNull(inputBuffer.data));
|
||||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
|
ByteBuffer outputData =
|
||||||
|
outputBuffer.init(inputBuffer.timeUs, streamMetadata.getMaxDecodedFrameSize());
|
||||||
try {
|
try {
|
||||||
decoderJni.decodeSample(outputData);
|
decoderJni.decodeSample(outputData);
|
||||||
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||||
return new FlacDecoderException("Frame decoding failed", e);
|
return new FlacDecoderException("Frame decoding failed", e);
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException e) {
|
||||||
// Never happens.
|
// Never happens.
|
||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
|
|
@ -121,4 +120,8 @@ import java.util.List;
|
||||||
decoderJni.release();
|
decoderJni.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the {@link FlacStreamMetadata} decoded from the initialization data. */
|
||||||
|
public FlacStreamMetadata getStreamMetadata() {
|
||||||
|
return streamMetadata;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.flac;
|
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. */
|
||||||
* Thrown when an Flac decoder error occurs.
|
public final class FlacDecoderException extends DecoderException {
|
||||||
*/
|
|
||||||
public final class FlacDecoderException extends AudioDecoderException {
|
|
||||||
|
|
||||||
/* package */ FlacDecoderException(String message) {
|
/* package */ FlacDecoderException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
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.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||||
import com.google.android.exoplayer2.util.FlacStreamMetadata;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
@ -51,12 +51,6 @@ import java.nio.ByteBuffer;
|
||||||
@Nullable private byte[] tempBuffer;
|
@Nullable private byte[] tempBuffer;
|
||||||
private boolean endOfExtractorInput;
|
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 {
|
public FlacDecoderJni() throws FlacDecoderException {
|
||||||
if (!FlacLibrary.isAvailable()) {
|
if (!FlacLibrary.isAvailable()) {
|
||||||
throw new FlacDecoderException("Failed to load decoder native libraries.");
|
throw new FlacDecoderException("Failed to load decoder native libraries.");
|
||||||
|
|
@ -121,7 +115,7 @@ import java.nio.ByteBuffer;
|
||||||
* read from the source, then 0 is returned.
|
* read from the source, then 0 is returned.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused") // Called from native code.
|
@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();
|
int byteCount = target.remaining();
|
||||||
if (byteBufferData != null) {
|
if (byteBufferData != null) {
|
||||||
byteCount = Math.min(byteCount, byteBufferData.remaining());
|
byteCount = Math.min(byteCount, byteBufferData.remaining());
|
||||||
|
|
@ -151,7 +145,7 @@ import java.nio.ByteBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Decodes and consumes the metadata from the FLAC stream. */
|
/** Decodes and consumes the metadata from the FLAC stream. */
|
||||||
public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
|
public FlacStreamMetadata decodeStreamMetadata() throws IOException {
|
||||||
FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
|
FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
|
||||||
if (streamMetadata == null) {
|
if (streamMetadata == null) {
|
||||||
throw new ParserException("Failed to decode stream metadata");
|
throw new ParserException("Failed to decode stream metadata");
|
||||||
|
|
@ -167,7 +161,7 @@ import java.nio.ByteBuffer;
|
||||||
* @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}.
|
* @param retryPosition If any error happens, the input will be rewound to {@code retryPosition}.
|
||||||
*/
|
*/
|
||||||
public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition)
|
public void decodeSampleWithBacktrackPosition(ByteBuffer output, long retryPosition)
|
||||||
throws InterruptedException, IOException, FlacFrameDecodeException {
|
throws IOException, FlacFrameDecodeException {
|
||||||
try {
|
try {
|
||||||
decodeSample(output);
|
decodeSample(output);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
@ -183,8 +177,7 @@ import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */
|
/** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */
|
||||||
@SuppressWarnings("ByteBufferBackingArray")
|
@SuppressWarnings("ByteBufferBackingArray")
|
||||||
public void decodeSample(ByteBuffer output)
|
public void decodeSample(ByteBuffer output) throws IOException, FlacFrameDecodeException {
|
||||||
throws IOException, InterruptedException, FlacFrameDecodeException {
|
|
||||||
output.clear();
|
output.clear();
|
||||||
int frameSize =
|
int frameSize =
|
||||||
output.isDirect()
|
output.isDirect()
|
||||||
|
|
@ -272,8 +265,7 @@ import java.nio.ByteBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int readFromExtractorInput(
|
private int readFromExtractorInput(
|
||||||
ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)
|
ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length) throws IOException {
|
||||||
throws IOException, InterruptedException {
|
|
||||||
int read = extractorInput.read(tempBuffer, offset, length);
|
int read = extractorInput.read(tempBuffer, offset, length);
|
||||||
if (read == C.RESULT_END_OF_INPUT) {
|
if (read == C.RESULT_END_OF_INPUT) {
|
||||||
endOfExtractorInput = true;
|
endOfExtractorInput = true;
|
||||||
|
|
@ -284,14 +276,11 @@ import java.nio.ByteBuffer;
|
||||||
|
|
||||||
private native long flacInit();
|
private native long flacInit();
|
||||||
|
|
||||||
private native FlacStreamMetadata flacDecodeMetadata(long context)
|
private native FlacStreamMetadata flacDecodeMetadata(long context) throws IOException;
|
||||||
throws IOException, InterruptedException;
|
|
||||||
|
|
||||||
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
|
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) throws IOException;
|
||||||
throws IOException, InterruptedException;
|
|
||||||
|
|
||||||
private native int flacDecodeToArray(long context, byte[] outputArray)
|
private native int flacDecodeToArray(long context, byte[] outputArray) throws IOException;
|
||||||
throws IOException, InterruptedException;
|
|
||||||
|
|
||||||
private native long flacGetDecodePosition(long context);
|
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.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||||
import com.google.android.exoplayer2.extractor.FlacMetadataReader;
|
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.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.extractor.SeekPoint;
|
import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.metadata.Metadata;
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
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.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -113,14 +113,13 @@ public final class FlacExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
|
public boolean sniff(ExtractorInput input) throws IOException {
|
||||||
id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
|
id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
|
||||||
return FlacMetadataReader.checkAndPeekStreamMarker(input);
|
return FlacMetadataReader.checkAndPeekStreamMarker(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException {
|
||||||
throws IOException, InterruptedException {
|
|
||||||
if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
|
if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
|
||||||
id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true);
|
id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true);
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +184,7 @@ public final class FlacExtractor implements Extractor {
|
||||||
@RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
|
@RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
|
||||||
@EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
|
@EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
|
||||||
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
|
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
|
||||||
private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
|
private void decodeStreamMetadata(ExtractorInput input) throws IOException {
|
||||||
if (streamMetadataDecoded) {
|
if (streamMetadataDecoded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -212,6 +211,7 @@ public final class FlacExtractor implements Extractor {
|
||||||
input.getLength(),
|
input.getLength(),
|
||||||
extractorOutput,
|
extractorOutput,
|
||||||
outputFrameHolder);
|
outputFrameHolder);
|
||||||
|
@Nullable
|
||||||
Metadata metadata = streamMetadata.getMetadataCopyWithAppendedEntriesFrom(id3Metadata);
|
Metadata metadata = streamMetadata.getMetadataCopyWithAppendedEntriesFrom(id3Metadata);
|
||||||
outputFormat(streamMetadata, metadata, trackOutput);
|
outputFormat(streamMetadata, metadata, trackOutput);
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +224,7 @@ public final class FlacExtractor implements Extractor {
|
||||||
ParsableByteArray outputBuffer,
|
ParsableByteArray outputBuffer,
|
||||||
OutputFrameHolder outputFrameHolder,
|
OutputFrameHolder outputFrameHolder,
|
||||||
TrackOutput trackOutput)
|
TrackOutput trackOutput)
|
||||||
throws InterruptedException, IOException {
|
throws IOException {
|
||||||
int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition);
|
int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition);
|
||||||
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
||||||
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
||||||
|
|
@ -249,7 +249,7 @@ public final class FlacExtractor implements Extractor {
|
||||||
SeekMap seekMap;
|
SeekMap seekMap;
|
||||||
if (haveSeekTable) {
|
if (haveSeekTable) {
|
||||||
seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni);
|
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();
|
long firstFramePosition = decoderJni.getDecodePosition();
|
||||||
binarySearchSeeker =
|
binarySearchSeeker =
|
||||||
new FlacBinarySearchSeeker(
|
new FlacBinarySearchSeeker(
|
||||||
|
|
@ -265,22 +265,16 @@ public final class FlacExtractor implements Extractor {
|
||||||
private static void outputFormat(
|
private static void outputFormat(
|
||||||
FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
|
FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
|
||||||
Format mediaFormat =
|
Format mediaFormat =
|
||||||
Format.createAudioSampleFormat(
|
new Format.Builder()
|
||||||
/* id= */ null,
|
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||||
MimeTypes.AUDIO_RAW,
|
.setAverageBitrate(streamMetadata.getDecodedBitrate())
|
||||||
/* codecs= */ null,
|
.setPeakBitrate(streamMetadata.getDecodedBitrate())
|
||||||
streamMetadata.getBitRate(),
|
.setMaxInputSize(streamMetadata.getMaxDecodedFrameSize())
|
||||||
streamMetadata.getMaxDecodedFrameSize(),
|
.setChannelCount(streamMetadata.channels)
|
||||||
streamMetadata.channels,
|
.setSampleRate(streamMetadata.sampleRate)
|
||||||
streamMetadata.sampleRate,
|
.setPcmEncoding(getPcmEncoding(streamMetadata.bitsPerSample))
|
||||||
getPcmEncoding(streamMetadata.bitsPerSample),
|
.setMetadata(metadata)
|
||||||
/* encoderDelay= */ 0,
|
.build();
|
||||||
/* encoderPadding= */ 0,
|
|
||||||
/* initializationData= */ null,
|
|
||||||
/* drmInitData= */ null,
|
|
||||||
/* selectionFlags= */ 0,
|
|
||||||
/* language= */ null,
|
|
||||||
metadata);
|
|
||||||
output.format(mediaFormat);
|
output.format(mediaFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,18 +21,25 @@ import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
import com.google.android.exoplayer2.audio.AudioSink;
|
||||||
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.drm.ExoMediaCrypto;
|
||||||
|
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.FlacConstants;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
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. */
|
||||||
* Decodes and renders audio using the native Flac decoder.
|
public final class LibflacAudioRenderer extends DecoderAudioRenderer {
|
||||||
*/
|
|
||||||
public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
|
||||||
|
|
||||||
|
private static final String TAG = "LibflacAudioRenderer";
|
||||||
private static final int NUM_BUFFERS = 16;
|
private static final int NUM_BUFFERS = 16;
|
||||||
|
|
||||||
|
private @MonotonicNonNull FlacStreamMetadata streamMetadata;
|
||||||
|
|
||||||
public LibflacAudioRenderer() {
|
public LibflacAudioRenderer() {
|
||||||
this(/* eventHandler= */ null, /* eventListener= */ null);
|
this(/* eventHandler= */ null, /* eventListener= */ null);
|
||||||
}
|
}
|
||||||
|
|
@ -50,15 +57,52 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
super(eventHandler, eventListener, audioProcessors);
|
super(eventHandler, eventListener, audioProcessors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||||
|
* null if delivery of events is not required.
|
||||||
|
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||||
|
* @param audioSink The sink to which audio will be output.
|
||||||
|
*/
|
||||||
|
public LibflacAudioRenderer(
|
||||||
|
@Nullable Handler eventHandler,
|
||||||
|
@Nullable AudioRendererEventListener eventListener,
|
||||||
|
AudioSink audioSink) {
|
||||||
|
super(
|
||||||
|
eventHandler,
|
||||||
|
eventListener,
|
||||||
|
audioSink);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int supportsFormatInternal(
|
public String getName() {
|
||||||
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@FormatSupport
|
||||||
|
protected int supportsFormatInternal(Format format) {
|
||||||
if (!FlacLibrary.isAvailable()
|
if (!FlacLibrary.isAvailable()
|
||||||
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
|
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
|
||||||
return FORMAT_UNSUPPORTED_TYPE;
|
return FORMAT_UNSUPPORTED_TYPE;
|
||||||
} else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) {
|
}
|
||||||
|
// Compute the PCM encoding that the FLAC decoder will output.
|
||||||
|
@C.PcmEncoding int pcmEncoding;
|
||||||
|
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;
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
if (!supportsOutput(format.channelCount, pcmEncoding)) {
|
||||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||||
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
|
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) {
|
||||||
return FORMAT_UNSUPPORTED_DRM;
|
return FORMAT_UNSUPPORTED_DRM;
|
||||||
} else {
|
} else {
|
||||||
return FORMAT_HANDLED;
|
return FORMAT_HANDLED;
|
||||||
|
|
@ -68,8 +112,22 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
@Override
|
@Override
|
||||||
protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
|
||||||
throws FlacDecoderException {
|
throws FlacDecoderException {
|
||||||
return new FlacDecoder(
|
TraceUtil.beginSection("createFlacDecoder");
|
||||||
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
|
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 new Format.Builder()
|
||||||
|
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||||
|
.setChannelCount(streamMetadata.channels)
|
||||||
|
.setSampleRate(streamMetadata.sampleRate)
|
||||||
|
.setPcmEncoding(Util.getPcmEncoding(streamMetadata.bitsPerSample))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
|
||||||
context->parser->getStreamInfo();
|
context->parser->getStreamInfo();
|
||||||
|
|
||||||
jclass flacStreamMetadataClass = env->FindClass(
|
jclass flacStreamMetadataClass = env->FindClass(
|
||||||
"com/google/android/exoplayer2/util/"
|
"com/google/android/exoplayer2/extractor/"
|
||||||
"FlacStreamMetadata");
|
"FlacStreamMetadata");
|
||||||
jmethodID flacStreamMetadataConstructor =
|
jmethodID flacStreamMetadataConstructor =
|
||||||
env->GetMethodID(flacStreamMetadataClass, "<init>",
|
env->GetMethodID(flacStreamMetadataClass, "<init>",
|
||||||
|
|
|
||||||
|
|
@ -349,26 +349,6 @@ bool FLACParser::decodeMetadata() {
|
||||||
ALOGE("unsupported bits per sample %u", getBitsPerSample());
|
ALOGE("unsupported bits per sample %u", getBitsPerSample());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// check sample rate
|
|
||||||
switch (getSampleRate()) {
|
|
||||||
case 8000:
|
|
||||||
case 11025:
|
|
||||||
case 12000:
|
|
||||||
case 16000:
|
|
||||||
case 22050:
|
|
||||||
case 24000:
|
|
||||||
case 32000:
|
|
||||||
case 44100:
|
|
||||||
case 48000:
|
|
||||||
case 88200:
|
|
||||||
case 96000:
|
|
||||||
case 176400:
|
|
||||||
case 192000:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ALOGE("unsupported sample rate %u", getSampleRate());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// configure the appropriate copy function based on device endianness.
|
// configure the appropriate copy function based on device endianness.
|
||||||
if (isBigEndian()) {
|
if (isBigEndian()) {
|
||||||
mCopy = copyToByteArrayBigEndian;
|
mCopy = copyToByteArrayBigEndian;
|
||||||
|
|
@ -462,8 +442,9 @@ bool FLACParser::getSeekPositions(int64_t timeUs,
|
||||||
if (sampleNumber <= targetSampleNumber) {
|
if (sampleNumber <= targetSampleNumber) {
|
||||||
result[0] = (sampleNumber * 1000000LL) / sampleRate;
|
result[0] = (sampleNumber * 1000000LL) / sampleRate;
|
||||||
result[1] = firstFrameOffset + points[i - 1].stream_offset;
|
result[1] = firstFrameOffset + points[i - 1].stream_offset;
|
||||||
if (sampleNumber == targetSampleNumber || i >= length) {
|
if (sampleNumber == targetSampleNumber || i >= length ||
|
||||||
// exact seek, or no following seek point.
|
points[i].sample_number == -1) { // placeholder
|
||||||
|
// exact seek, or no following non-placeholder seek point
|
||||||
result[2] = result[0];
|
result[2] = result[0];
|
||||||
result[3] = result[1];
|
result[3] = result[1];
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2016 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.ext.flac;
|
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
|
|
||||||
import com.google.android.exoplayer2.extractor.wav.WavExtractor;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
/** Unit test for {@link DefaultExtractorsFactory}. */
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public final class DefaultExtractorsFactoryTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreateExtractors_returnExpectedClasses() {
|
|
||||||
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
|
|
||||||
|
|
||||||
Extractor[] extractors = defaultExtractorsFactory.createExtractors();
|
|
||||||
List<Class<?>> listCreatedExtractorClasses = new ArrayList<>();
|
|
||||||
for (Extractor extractor : extractors) {
|
|
||||||
listCreatedExtractorClasses.add(extractor.getClass());
|
|
||||||
}
|
|
||||||
|
|
||||||
Class<?>[] expectedExtractorClassses =
|
|
||||||
new Class<?>[] {
|
|
||||||
MatroskaExtractor.class,
|
|
||||||
FragmentedMp4Extractor.class,
|
|
||||||
Mp4Extractor.class,
|
|
||||||
Mp3Extractor.class,
|
|
||||||
AdtsExtractor.class,
|
|
||||||
Ac3Extractor.class,
|
|
||||||
Ac4Extractor.class,
|
|
||||||
TsExtractor.class,
|
|
||||||
FlvExtractor.class,
|
|
||||||
OggExtractor.class,
|
|
||||||
PsExtractor.class,
|
|
||||||
WavExtractor.class,
|
|
||||||
AmrExtractor.class,
|
|
||||||
FlacExtractor.class
|
|
||||||
};
|
|
||||||
|
|
||||||
assertThat(listCreatedExtractorClasses).containsNoDuplicates();
|
|
||||||
assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -36,6 +36,7 @@ dependencies {
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
api 'com.google.vr:sdk-base:1.190.0'
|
api 'com.google.vr:sdk-base:1.190.0'
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import java.nio.ByteOrder;
|
||||||
* href="https://github.com/google/ExoPlayer/issues">issue tracker</a>.
|
* href="https://github.com/google/ExoPlayer/issues">issue tracker</a>.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public final class GvrAudioProcessor implements AudioProcessor {
|
public class GvrAudioProcessor implements AudioProcessor {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ExoPlayerLibraryInfo.registerModule("goog.exo.gvr");
|
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;
|
||||||
|
|
@ -58,7 +58,9 @@ playback.
|
||||||
|
|
||||||
## Links ##
|
## Links ##
|
||||||
|
|
||||||
|
* [ExoPlayer documentation on ad insertion][]
|
||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
|
[ExoPlayer documentation on ad insertion]: https://exoplayer.dev/ad-insertion.html
|
||||||
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,29 @@ android {
|
||||||
minSdkVersion project.ext.minSdkVersion
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
consumerProguardFiles 'proguard-rules.txt'
|
consumerProguardFiles 'proguard-rules.txt'
|
||||||
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
// Enable multidex for androidTests.
|
||||||
|
multiDexEnabled true
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
androidTest.assets.srcDir '../../testdata/src/test/assets/'
|
||||||
}
|
}
|
||||||
|
|
||||||
testOptions.unitTests.includeAndroidResources = true
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3'
|
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.18.1'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
|
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
|
||||||
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
|
androidTestImplementation project(modulePrefix + 'testutils')
|
||||||
|
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
|
||||||
|
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||||
|
androidTestImplementation 'com.android.support:multidex:1.0.3'
|
||||||
|
androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
extensions/ima/src/androidTest/AndroidManifest.xml
Normal file
38
extensions/ima/src/androidTest/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="com.google.android.exoplayer2.ext.ima.test">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
<uses-sdk/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||||
|
<activity android:name="com.google.android.exoplayer2.testutil.HostActivity"
|
||||||
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
|
android:label="ExoPlayerTest"/>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<instrumentation
|
||||||
|
android:targetPackage="com.google.android.exoplayer2.ext.ima.test"
|
||||||
|
android:name="androidx.test.runner.AndroidJUnitRunner"/>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,283 @@
|
||||||
|
/*
|
||||||
|
* 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.ext.ima;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.rule.ActivityTestRule;
|
||||||
|
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.Window;
|
||||||
|
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||||
|
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider;
|
||||||
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
|
import com.google.android.exoplayer2.testutil.ActionSchedule;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExoHostedTest;
|
||||||
|
import com.google.android.exoplayer2.testutil.HostActivity;
|
||||||
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Playback tests using {@link ImaAdsLoader}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class ImaPlaybackTest {
|
||||||
|
|
||||||
|
private static final String TAG = "ImaPlaybackTest";
|
||||||
|
|
||||||
|
private static final long TIMEOUT_MS = 5 * 60 * C.MILLIS_PER_SECOND;
|
||||||
|
|
||||||
|
private static final String CONTENT_URI_SHORT =
|
||||||
|
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4";
|
||||||
|
private static final String CONTENT_URI_LONG =
|
||||||
|
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-25s.mp4";
|
||||||
|
private static final AdId CONTENT = new AdId(C.INDEX_UNSET, C.INDEX_UNSET);
|
||||||
|
|
||||||
|
@Rule public ActivityTestRule<HostActivity> testRule = new ActivityTestRule<>(HostActivity.class);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackWithPrerollAdTag_playsAdAndContent() throws Exception {
|
||||||
|
String adsResponse =
|
||||||
|
TestUtil.getString(/* context= */ testRule.getActivity(), "ad-responses/preroll.xml");
|
||||||
|
AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT};
|
||||||
|
ImaHostedTest hostedTest =
|
||||||
|
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
|
||||||
|
|
||||||
|
testRule.getActivity().runTest(hostedTest, TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackWithMidrolls_playsAdAndContent() throws Exception {
|
||||||
|
String adsResponse =
|
||||||
|
TestUtil.getString(
|
||||||
|
/* context= */ testRule.getActivity(), "ad-responses/preroll_midroll6s_postroll.xml");
|
||||||
|
AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT, ad(1), CONTENT, ad(2), CONTENT};
|
||||||
|
ImaHostedTest hostedTest =
|
||||||
|
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
|
||||||
|
|
||||||
|
testRule.getActivity().runTest(hostedTest, TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackWithMidrolls1And7_playsAdsAndContent() throws Exception {
|
||||||
|
String adsResponse =
|
||||||
|
TestUtil.getString(
|
||||||
|
/* context= */ testRule.getActivity(), "ad-responses/midroll1s_midroll7s.xml");
|
||||||
|
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
|
||||||
|
ImaHostedTest hostedTest =
|
||||||
|
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
|
||||||
|
|
||||||
|
testRule.getActivity().runTest(hostedTest, TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackWithMidrolls10And20WithSeekTo12_playsAdsAndContent() throws Exception {
|
||||||
|
String adsResponse =
|
||||||
|
TestUtil.getString(
|
||||||
|
/* context= */ testRule.getActivity(), "ad-responses/midroll10s_midroll20s.xml");
|
||||||
|
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
|
||||||
|
ImaHostedTest hostedTest =
|
||||||
|
new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds);
|
||||||
|
hostedTest.setSchedule(
|
||||||
|
new ActionSchedule.Builder(TAG)
|
||||||
|
.waitForPlaybackState(Player.STATE_READY)
|
||||||
|
.seek(12 * C.MILLIS_PER_SECOND)
|
||||||
|
.build());
|
||||||
|
testRule.getActivity().runTest(hostedTest, TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("The second ad doesn't preload so playback gets stuck. See [internal: b/155615925].")
|
||||||
|
@Test
|
||||||
|
public void playbackWithMidrolls10And20WithSeekTo18_playsAdsAndContent() throws Exception {
|
||||||
|
String adsResponse =
|
||||||
|
TestUtil.getString(
|
||||||
|
/* context= */ testRule.getActivity(), "ad-responses/midroll10s_midroll20s.xml");
|
||||||
|
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
|
||||||
|
ImaHostedTest hostedTest =
|
||||||
|
new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds);
|
||||||
|
hostedTest.setSchedule(
|
||||||
|
new ActionSchedule.Builder(TAG)
|
||||||
|
.waitForPlaybackState(Player.STATE_READY)
|
||||||
|
.seek(18 * C.MILLIS_PER_SECOND)
|
||||||
|
.build());
|
||||||
|
testRule.getActivity().runTest(hostedTest, TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AdId ad(int groupIndex) {
|
||||||
|
return new AdId(groupIndex, /* indexInGroup= */ 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AdId {
|
||||||
|
|
||||||
|
public final int groupIndex;
|
||||||
|
public final int indexInGroup;
|
||||||
|
|
||||||
|
public AdId(int groupIndex, int indexInGroup) {
|
||||||
|
this.groupIndex = groupIndex;
|
||||||
|
this.indexInGroup = indexInGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "(" + groupIndex + ", " + indexInGroup + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdId that = (AdId) o;
|
||||||
|
|
||||||
|
if (groupIndex != that.groupIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return indexInGroup == that.indexInGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = groupIndex;
|
||||||
|
result = 31 * result + indexInGroup;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ImaHostedTest extends ExoHostedTest implements EventListener {
|
||||||
|
|
||||||
|
private final Uri contentUri;
|
||||||
|
private final String adsResponse;
|
||||||
|
private final List<AdId> expectedAdIds;
|
||||||
|
private final List<AdId> seenAdIds;
|
||||||
|
private @MonotonicNonNull ImaAdsLoader imaAdsLoader;
|
||||||
|
private @MonotonicNonNull SimpleExoPlayer player;
|
||||||
|
|
||||||
|
private ImaHostedTest(Uri contentUri, String adsResponse, AdId... expectedAdIds) {
|
||||||
|
// fullPlaybackNoSeeking is false as the playback lasts longer than the content source
|
||||||
|
// duration due to ad playback, so the hosted test shouldn't assert the playing duration.
|
||||||
|
super(ImaPlaybackTest.class.getSimpleName(), /* fullPlaybackNoSeeking= */ false);
|
||||||
|
this.contentUri = contentUri;
|
||||||
|
this.adsResponse = adsResponse;
|
||||||
|
this.expectedAdIds = Arrays.asList(expectedAdIds);
|
||||||
|
seenAdIds = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SimpleExoPlayer buildExoPlayer(
|
||||||
|
HostActivity host, Surface surface, MappingTrackSelector trackSelector) {
|
||||||
|
player = super.buildExoPlayer(host, surface, trackSelector);
|
||||||
|
player.addAnalyticsListener(
|
||||||
|
new AnalyticsListener() {
|
||||||
|
@Override
|
||||||
|
public void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) {
|
||||||
|
maybeUpdateSeenAdIdentifiers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity(
|
||||||
|
EventTime eventTime, @DiscontinuityReason int reason) {
|
||||||
|
if (reason != Player.DISCONTINUITY_REASON_SEEK) {
|
||||||
|
maybeUpdateSeenAdIdentifiers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Context context = host.getApplicationContext();
|
||||||
|
imaAdsLoader = new ImaAdsLoader.Builder(context).buildForAdsResponse(adsResponse);
|
||||||
|
imaAdsLoader.setPlayer(player);
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MediaSource buildSource(
|
||||||
|
HostActivity host,
|
||||||
|
String userAgent,
|
||||||
|
DrmSessionManager drmSessionManager,
|
||||||
|
FrameLayout overlayFrameLayout) {
|
||||||
|
Context context = host.getApplicationContext();
|
||||||
|
DataSource.Factory dataSourceFactory =
|
||||||
|
new DefaultDataSourceFactory(
|
||||||
|
context, Util.getUserAgent(context, ImaPlaybackTest.class.getSimpleName()));
|
||||||
|
MediaSource contentMediaSource =
|
||||||
|
DefaultMediaSourceFactory.newInstance(context)
|
||||||
|
.createMediaSource(MediaItem.fromUri(contentUri));
|
||||||
|
return new AdsMediaSource(
|
||||||
|
contentMediaSource,
|
||||||
|
dataSourceFactory,
|
||||||
|
Assertions.checkNotNull(imaAdsLoader),
|
||||||
|
new AdViewProvider() {
|
||||||
|
@Override
|
||||||
|
public ViewGroup getAdViewGroup() {
|
||||||
|
return overlayFrameLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View[] getAdOverlayViews() {
|
||||||
|
return new View[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) {
|
||||||
|
assertThat(seenAdIds).isEqualTo(expectedAdIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeUpdateSeenAdIdentifiers() {
|
||||||
|
if (Assertions.checkNotNull(player)
|
||||||
|
.getCurrentTimeline()
|
||||||
|
.getWindow(/* windowIndex= */ 0, new Window())
|
||||||
|
.isPlaceholder) {
|
||||||
|
// The window is still an initial placeholder so do nothing.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AdId adId = new AdId(player.getCurrentAdGroupIndex(), player.getCurrentAdIndexInAdGroup());
|
||||||
|
if (seenAdIds.isEmpty() || !seenAdIds.get(seenAdIds.size() - 1).equals(adId)) {
|
||||||
|
seenAdIds.add(adId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,211 +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.ima;
|
|
||||||
|
|
||||||
import com.google.ads.interactivemedia.v3.api.Ad;
|
|
||||||
import com.google.ads.interactivemedia.v3.api.AdPodInfo;
|
|
||||||
import com.google.ads.interactivemedia.v3.api.CompanionAd;
|
|
||||||
import com.google.ads.interactivemedia.v3.api.UiElement;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/** A fake ad for testing. */
|
|
||||||
/* package */ final class FakeAd implements Ad {
|
|
||||||
|
|
||||||
private final boolean skippable;
|
|
||||||
private final AdPodInfo adPodInfo;
|
|
||||||
|
|
||||||
public FakeAd(boolean skippable, int podIndex, int totalAds, int adPosition) {
|
|
||||||
this.skippable = skippable;
|
|
||||||
adPodInfo =
|
|
||||||
new AdPodInfo() {
|
|
||||||
@Override
|
|
||||||
public int getTotalAds() {
|
|
||||||
return totalAds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getAdPosition() {
|
|
||||||
return adPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPodIndex() {
|
|
||||||
return podIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isBumper() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double getMaxDuration() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double getTimeOffset() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getVastMediaWidth() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getVastMediaHeight() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getVastMediaBitrate() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSkippable() {
|
|
||||||
return skippable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AdPodInfo getAdPodInfo() {
|
|
||||||
return adPodInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAdId() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getCreativeId() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getCreativeAdId() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUniversalAdIdValue() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUniversalAdIdRegistry() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAdSystem() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getAdWrapperIds() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getAdWrapperSystems() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getAdWrapperCreativeIds() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLinear() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double getSkipTimeOffset() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUiDisabled() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getTitle() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getContentType() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAdvertiserName() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSurveyUrl() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDealId() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getWidth() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getHeight() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getTraffickingParameters() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public double getDuration() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<UiElement> getUiElements() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CompanionAd> getCompanionAds() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue