Commit 76962d50 by Oliver Woodman Committed by GitHub

Merge pull request #6752 from google/dev-v2-r2.11.0

r2.11.0
parents 33938c05 4a4aa16d
Showing with 2191 additions and 836 deletions
...@@ -57,6 +57,9 @@ extensions/vp9/src/main/jni/libvpx ...@@ -57,6 +57,9 @@ extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv extensions/vp9/src/main/jni/libyuv
# AV1 extension
extensions/av1/src/main/jni/libgav1
# Opus extension # Opus extension
extensions/opus/src/main/jni/libopus extensions/opus/src/main/jni/libopus
......
...@@ -62,6 +62,9 @@ extensions/vp9/src/main/jni/libvpx ...@@ -62,6 +62,9 @@ extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv extensions/vp9/src/main/jni/libyuv
# AV1 extension
extensions/av1/src/main/jni/libgav1
# Opus extension # Opus extension
extensions/opus/src/main/jni/libopus extensions/opus/src/main/jni/libopus
......
...@@ -13,17 +13,30 @@ ...@@ -13,17 +13,30 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.10.8' releaseVersion = '2.11.0'
releaseVersionCode = 2010008 releaseVersionCode = 2011000
minSdkVersion = 16 minSdkVersion = 16
targetSdkVersion = 28 appTargetSdkVersion = 29
compileSdkVersion = 28 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
compileSdkVersion = 29
dexmakerVersion = '2.21.0' dexmakerVersion = '2.21.0'
guavaVersion = '23.5-android'
mockitoVersion = '2.25.0' mockitoVersion = '2.25.0'
robolectricVersion = '4.2' robolectricVersion = '4.3'
autoValueVersion = '1.6' autoValueVersion = '1.6'
autoServiceVersion = '1.0-rc4'
checkerframeworkVersion = '2.5.0' checkerframeworkVersion = '2.5.0'
androidXTestVersion = '1.1.0' jsr305Version = '3.0.2'
kotlinAnnotationsVersion = '1.3.31'
androidxAnnotationVersion = '1.1.0'
androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0'
androidxMediaVersion = '1.0.1'
androidxTestCoreVersion = '1.2.0'
androidxTestJUnitVersion = '1.1.1'
androidxTestRunnerVersion = '1.2.0'
androidxTestRulesVersion = '1.2.0'
truthVersion = '0.44'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix
......
...@@ -24,7 +24,7 @@ include modulePrefix + 'library-hls' ...@@ -24,7 +24,7 @@ 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 + 'testutils-robolectric' include modulePrefix + 'extension-av1'
include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac' include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr' include modulePrefix + 'extension-gvr'
...@@ -47,7 +47,7 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl ...@@ -47,7 +47,7 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl
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 + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric') 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')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
......
...@@ -26,7 +26,7 @@ android { ...@@ -26,7 +26,7 @@ android {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
...@@ -56,10 +56,9 @@ dependencies { ...@@ -56,10 +56,9 @@ dependencies {
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast') implementation project(modulePrefix + 'extension-cast')
implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
...@@ -16,87 +16,86 @@ ...@@ -16,87 +16,86 @@
package com.google.android.exoplayer2.castdemo; package com.google.android.exoplayer2.castdemo;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable;
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.ext.cast.MediaItem.DrmConfiguration;
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;
import java.util.List; import java.util.List;
import java.util.UUID;
/** Utility methods and constants for the Cast demo application. */ /** Utility methods and constants for the Cast demo application. */
/* package */ final class DemoUtil { /* package */ final class DemoUtil {
/** Represents a media sample. */
public static final class Sample {
/** The uri of the media content. */
public final String uri;
/** The name of the sample. */
public final String name;
/** The mime type of the sample media content. */
public final String mimeType;
/**
* The {@link UUID} of the DRM scheme that protects the content, or null if the content is not
* DRM-protected.
*/
@Nullable public final UUID drmSchemeUuid;
/**
* The url from which players should obtain DRM licenses, or null if the content is not
* DRM-protected.
*/
@Nullable public final Uri licenseServerUri;
/**
* @param uri See {@link #uri}.
* @param name See {@link #name}.
* @param mimeType See {@link #mimeType}.
*/
public Sample(String uri, String name, String mimeType) {
this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null);
}
public Sample(
String uri,
String name,
String mimeType,
@Nullable UUID drmSchemeUuid,
@Nullable String licenseServerUriString) {
this.uri = uri;
this.name = name;
this.mimeType = mimeType;
this.drmSchemeUuid = drmSchemeUuid;
this.licenseServerUri =
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
}
@Override
public String toString() {
return name;
}
}
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/** The list of samples available in the cast demo app. */ /** The list of samples available in the cast demo app. */
public static final List<Sample> SAMPLES; public static final List<MediaItem> SAMPLES;
static { static {
// App samples. ArrayList<MediaItem> samples = new ArrayList<>();
ArrayList<Sample> samples = new ArrayList<>();
// Clear content. // Clear content.
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
"Clear DASH: Tears", .setTitle("Clear DASH: Tears")
MIME_TYPE_DASH)); .setMimeType(MIME_TYPE_DASH)
.build());
samples.add(
new MediaItem.Builder()
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
.setTitle("Clear HLS: Angel one")
.setMimeType(MIME_TYPE_HLS)
.build());
samples.add(
new MediaItem.Builder()
.setUri("https://html5demos.com/assets/dizzy.mp4")
.setTitle("Clear MP4: Dizzy")
.setMimeType(MIME_TYPE_VIDEO_MP4)
.build());
// DRM content.
samples.add(
new MediaItem.Builder()
.setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"))
.setTitle("Widevine DASH cenc: Tears")
.setMimeType(MIME_TYPE_DASH)
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
Collections.emptyMap()))
.build());
samples.add(
new MediaItem.Builder()
.setUri(
Uri.parse(
"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"))
.setTitle("Widevine DASH cbc1: Tears")
.setMimeType(MIME_TYPE_DASH)
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
Collections.emptyMap()))
.build());
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); .setUri(
Uri.parse(
"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"))
.setTitle("Widevine DASH cbcs: Tears")
.setMimeType(MIME_TYPE_DASH)
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
Collections.emptyMap()))
.build());
SAMPLES = Collections.unmodifiableList(samples); SAMPLES = Collections.unmodifiableList(samples);
} }
......
...@@ -17,13 +17,6 @@ package com.google.android.exoplayer2.castdemo; ...@@ -17,13 +17,6 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import androidx.core.graphics.ColorUtils;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
...@@ -34,16 +27,23 @@ import android.widget.ArrayAdapter; ...@@ -34,16 +27,23 @@ import android.widget.ArrayAdapter;
import android.widget.ListView; import android.widget.ListView;
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.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.ColorUtils;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
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.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.gms.cast.CastMediaControlIntent;
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;
import java.util.Collections;
/** /**
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
...@@ -52,8 +52,6 @@ import java.util.Collections; ...@@ -52,8 +52,6 @@ import java.util.Collections;
public class MainActivity extends AppCompatActivity public class MainActivity extends AppCompatActivity
implements OnClickListener, PlayerManager.Listener { implements OnClickListener, PlayerManager.Listener {
private final MediaItem.Builder mediaItemBuilder;
private PlayerView localPlayerView; private PlayerView localPlayerView;
private PlayerControlView castControlView; private PlayerControlView castControlView;
private PlayerManager playerManager; private PlayerManager playerManager;
...@@ -61,10 +59,6 @@ public class MainActivity extends AppCompatActivity ...@@ -61,10 +59,6 @@ public class MainActivity extends AppCompatActivity
private MediaQueueListAdapter mediaQueueListAdapter; private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext; private CastContext castContext;
public MainActivity() {
mediaItemBuilder = new MediaItem.Builder();
}
// Activity lifecycle methods. // Activity lifecycle methods.
@Override @Override
...@@ -118,20 +112,13 @@ public class MainActivity extends AppCompatActivity ...@@ -118,20 +112,13 @@ public class MainActivity extends AppCompatActivity
// There is no Cast context to work with. Do nothing. // There is no Cast context to work with. Do nothing.
return; return;
} }
String applicationId = castContext.getCastOptions().getReceiverApplicationId();
switch (applicationId) {
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID:
playerManager = playerManager =
new DefaultReceiverPlayerManager( new PlayerManager(
/* listener= */ this, /* listener= */ this,
localPlayerView, localPlayerView,
castControlView, castControlView,
/* context= */ this, /* context= */ this,
castContext); castContext);
break;
default:
throw new IllegalStateException("Illegal receiver app id: " + applicationId);
}
mediaQueueList.setAdapter(mediaQueueListAdapter); mediaQueueList.setAdapter(mediaQueueListAdapter);
} }
...@@ -179,36 +166,29 @@ public class MainActivity extends AppCompatActivity ...@@ -179,36 +166,29 @@ public class MainActivity extends AppCompatActivity
} }
@Override @Override
public void onQueueContentsExternallyChanged() { public void onUnsupportedTrack(int trackType) {
mediaQueueListAdapter.notifyDataSetChanged(); if (trackType == C.TRACK_TYPE_AUDIO) {
showToast(R.string.error_unsupported_audio);
} else if (trackType == C.TRACK_TYPE_VIDEO) {
showToast(R.string.error_unsupported_video);
} else {
// Do nothing.
} }
@Override
public void onPlayerError() {
Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show();
} }
// Internal methods. // Internal methods.
private void showToast(int messageId) {
Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show();
}
private View buildSampleListView() { private View buildSampleListView() {
View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);
ListView sampleList = dialogList.findViewById(R.id.sample_list); ListView sampleList = dialogList.findViewById(R.id.sample_list);
sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener( sampleList.setOnItemClickListener(
(parent, view, position, id) -> { (parent, view, position, id) -> {
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); playerManager.addItem(DemoUtil.SAMPLES.get(position));
mediaItemBuilder
.clear()
.setMedia(sample.uri)
.setTitle(sample.name)
.setMimeType(sample.mimeType);
if (sample.drmSchemeUuid != null) {
mediaItemBuilder.setDrmSchemes(
Collections.singletonList(
new MediaItem.DrmScheme(
sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));
}
playerManager.addItem(mediaItemBuilder.build());
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
}); });
return dialogList; return dialogList;
...@@ -231,7 +211,9 @@ public class MainActivity extends AppCompatActivity ...@@ -231,7 +211,9 @@ public class MainActivity extends AppCompatActivity
TextView view = holder.textView; TextView view = holder.textView;
view.setText(holder.item.title); view.setText(holder.item.title);
// TODO: Solve coloring using the theme's ColorStateList. // TODO: Solve coloring using the theme's ColorStateList.
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), view.setTextColor(
ColorUtils.setAlphaComponent(
view.getCurrentTextColor(),
position == playerManager.getCurrentItemIndex() ? 255 : 100)); position == playerManager.getCurrentItemIndex() ? 255 : 100));
} }
...@@ -312,11 +294,18 @@ public class MainActivity extends AppCompatActivity ...@@ -312,11 +294,18 @@ public class MainActivity extends AppCompatActivity
} }
} }
private static final class SampleListAdapter extends ArrayAdapter<DemoUtil.Sample> { private static final class SampleListAdapter extends ArrayAdapter<MediaItem> {
public SampleListAdapter(Context context) { public SampleListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
} }
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = super.getView(position, convertView, parent);
((TextView) view).setText(getItem(position).title);
return view;
}
}
} }
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string> <string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
<string name="player_error_msg">Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info.</string> <string name="error_unsupported_video">Media includes video tracks, but none are playable by this device</string>
<string name="error_unsupported_audio">Media includes audio tracks, but none are playable by this device</string>
</resources> </resources>
# IMA demo application #
This folder contains a demo application that showcases ExoPlayer integration
with the IMA SDK.
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.imademo;
import android.app.Activity;
import android.os.Bundle;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ui.PlayerView;
/**
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
* {@link PlayerManager}, which this class instantiates.
*/
public final class MainActivity extends Activity {
private PlayerView playerView;
private PlayerManager player;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
playerView = findViewById(R.id.player_view);
player = new PlayerManager(this);
}
@Override
public void onResume() {
super.onResume();
player.init(this, playerView);
}
@Override
public void onPause() {
super.onPause();
player.reset();
}
@Override
public void onDestroy() {
player.release();
super.onDestroy();
}
}
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.imademo;
import android.content.Context;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
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.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
private final ImaAdsLoader adsLoader;
private final DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
private long contentPosition;
public PlayerManager(Context context) {
String adTag = context.getString(R.string.ad_tag_url);
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
dataSourceFactory =
new DefaultDataSourceFactory(
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
}
public void init(Context context, PlayerView playerView) {
// Create a player instance.
player = ExoPlayerFactory.newSimpleInstance(context);
adsLoader.setPlayer(player);
playerView.setPlayer(player);
// This is the MediaSource representing the content media (i.e. not the ad).
String contentUrl = context.getString(R.string.content_url);
MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));
// Compose the content media source into a new AdsMediaSource with both ads and content.
MediaSource mediaSourceWithAds =
new AdsMediaSource(
contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, playerView);
// Prepare the player with the source.
player.seekTo(contentPosition);
player.prepare(mediaSourceWithAds);
player.setPlayWhenReady(true);
}
public void reset() {
if (player != null) {
contentPosition = player.getContentPosition();
player.release();
player = null;
adsLoader.setPlayer(null);
}
}
public void release() {
if (player != null) {
player.release();
player = null;
}
adsLoader.release();
}
// AdsMediaSource.MediaSourceFactory implementation.
@Override
public MediaSource createMediaSource(Uri uri) {
return buildMediaSource(uri);
}
@Override
public int[] getSupportedTypes() {
// IMA does not support Smooth Streaming ads.
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
}
// Internal methods.
private MediaSource buildMediaSource(Uri uri) {
@ContentType int type = Util.inferContentType(uri);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_OTHER:
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.google.android.exoplayer2.ui.PlayerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true"/>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="application_name">Exo IMA Demo</string>
<string name="content_url"><![CDATA[https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv]]></string>
<string name="ad_tag_url"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="PlayerTheme" parent="android:Theme.Holo">
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
</resources>
...@@ -26,7 +26,7 @@ android { ...@@ -26,7 +26,7 @@ android {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
...@@ -62,15 +62,15 @@ android { ...@@ -62,15 +62,15 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.legacy:legacy-support-core-ui:1.0.0' implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.fragment:fragment:1.0.0'
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.0.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')
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg') withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
withExtensionsImplementation project(path: modulePrefix + 'extension-flac') withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
withExtensionsImplementation project(path: modulePrefix + 'extension-ima') withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
android:banner="@drawable/ic_banner" android:banner="@drawable/ic_banner"
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"
android:requestLegacyExternalStorage="true"
android:name="com.google.android.exoplayer2.demo.DemoApplication" android:name="com.google.android.exoplayer2.demo.DemoApplication"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
......
...@@ -376,44 +376,48 @@ ...@@ -376,44 +376,48 @@
"uri": "https://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"name": "Apple AAC 10s", "name": "Apple 10s (AAC)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
}, },
{ {
"name": "Apple TS 10s", "name": "Apple 10s (TS)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
}, },
{ {
"name": "Android screens (Matroska)", "name": "Android screens (MKV)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
}, },
{ {
"name": "Screens 360P (WebM,VP9,No Audio)", "name": "Screens 360p video (WebM,VP9)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
}, },
{ {
"name": "Screens 480p (FMP4,H264,No Audio)", "name": "Screens 480p video (FMP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
}, },
{ {
"name": "Screens 1080p (FMP4,H264, No Audio)", "name": "Screens 1080p video (FMP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
}, },
{ {
"name": "Screens (FMP4,AAC Audio)", "name": "Screens audio (FMP4)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
}, },
{ {
"name": "Google Play (MP3 Audio)", "name": "Google Play (MP3)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3" "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
}, },
{ {
"name": "Google Play (Ogg/Vorbis Audio)", "name": "Google Play (Ogg/Vorbis)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
}, },
{ {
"name": "Big Buck Bunny (FLV Video)", "name": "Big Buck Bunny video (FLV)",
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
},
{
"name": "Big Buck Bunny 480p video (MP4,AV1)",
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
} }
] ]
}, },
...@@ -447,23 +451,27 @@ ...@@ -447,23 +451,27 @@
}, },
{ {
"name": "Clear -> Enc -> Clear -> Enc -> Enc", "name": "Clear -> Enc -> Clear -> Enc -> Enc",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
"playlist": [ "playlist": [
{ {
"uri": "https://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"uri": "https://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
} }
] ]
} }
...@@ -578,5 +586,17 @@ ...@@ -578,5 +586,17 @@
"spherical_stereo_mode": "top_bottom" "spherical_stereo_mode": "top_bottom"
} }
] ]
},
{
"name": "Subtitles",
"samples": [
{
"name": "TTML",
"uri": "https://html5demos.com/assets/dizzy.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
"subtitle_mime_type": "application/ttml+xml",
"subtitle_language": "en"
}
]
} }
] ]
...@@ -28,7 +28,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; ...@@ -28,7 +28,7 @@ import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
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.FileDataSourceFactory; 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;
...@@ -165,7 +165,7 @@ public class DemoApplication extends Application { ...@@ -165,7 +165,7 @@ public class DemoApplication extends Application {
return new CacheDataSourceFactory( return new CacheDataSourceFactory(
cache, cache,
upstreamFactory, upstreamFactory,
new FileDataSourceFactory(), new FileDataSource.Factory(),
/* cacheWriteDataSinkFactory= */ null, /* cacheWriteDataSinkFactory= */ null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
/* eventListener= */ null); /* eventListener= */ null);
......
...@@ -18,9 +18,9 @@ package com.google.android.exoplayer2.demo; ...@@ -18,9 +18,9 @@ package com.google.android.exoplayer2.demo;
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 androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import android.widget.Toast;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
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;
...@@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadIndex; ...@@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadIndex;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
...@@ -55,6 +56,7 @@ public class DownloadTracker { ...@@ -55,6 +56,7 @@ public class DownloadTracker {
private final CopyOnWriteArraySet<Listener> listeners; private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, Download> downloads; private final HashMap<Uri, Download> downloads;
private final DownloadIndex downloadIndex; private final DownloadIndex downloadIndex;
private final DefaultTrackSelector.Parameters trackSelectorParameters;
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper; @Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
...@@ -65,6 +67,7 @@ public class DownloadTracker { ...@@ -65,6 +67,7 @@ public class DownloadTracker {
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
downloads = new HashMap<>(); downloads = new HashMap<>();
downloadIndex = downloadManager.getDownloadIndex(); downloadIndex = downloadManager.getDownloadIndex();
trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
downloadManager.addListener(new DownloadManagerListener()); downloadManager.addListener(new DownloadManagerListener());
loadDownloads(); loadDownloads();
} }
...@@ -82,7 +85,6 @@ public class DownloadTracker { ...@@ -82,7 +85,6 @@ public class DownloadTracker {
return download != null && download.state != Download.STATE_FAILED; return download != null && download.state != Download.STATE_FAILED;
} }
@SuppressWarnings("unchecked")
public DownloadRequest getDownloadRequest(Uri uri) { public DownloadRequest getDownloadRequest(Uri uri) {
Download download = downloads.get(uri); Download download = downloads.get(uri);
return download != null && download.state != Download.STATE_FAILED ? download.request : null; return download != null && download.state != Download.STATE_FAILED ? download.request : null;
...@@ -124,13 +126,13 @@ public class DownloadTracker { ...@@ -124,13 +126,13 @@ public class DownloadTracker {
int type = Util.inferContentType(uri, extension); int type = Util.inferContentType(uri, extension);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS: case C.TYPE_SS:
return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS: case C.TYPE_HLS:
return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return DownloadHelper.forProgressive(uri); return DownloadHelper.forProgressive(context, uri);
default: default:
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }
...@@ -203,7 +205,7 @@ public class DownloadTracker { ...@@ -203,7 +205,7 @@ public class DownloadTracker {
TrackSelectionDialog.createForMappedTrackInfoAndParameters( TrackSelectionDialog.createForMappedTrackInfoAndParameters(
/* titleId= */ R.string.exo_download_description, /* titleId= */ R.string.exo_download_description,
mappedTrackInfo, mappedTrackInfo,
/* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters,
/* allowAdaptiveSelections =*/ false, /* allowAdaptiveSelections =*/ false,
/* allowMultipleOverrides= */ true, /* allowMultipleOverrides= */ true,
/* onClickListener= */ this, /* onClickListener= */ this,
...@@ -213,10 +215,13 @@ public class DownloadTracker { ...@@ -213,10 +215,13 @@ public class DownloadTracker {
@Override @Override
public void onPrepareError(DownloadHelper helper, IOException e) { public void onPrepareError(DownloadHelper helper, IOException e) {
Toast.makeText( Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) Log.e(
.show(); TAG,
Log.e(TAG, "Failed to start download", e); e instanceof DownloadHelper.LiveContentUnsupportedException
? "Downloading live content unsupported"
: "Failed to start download",
e);
} }
// DialogInterface.OnClickListener implementation. // DialogInterface.OnClickListener implementation.
...@@ -230,7 +235,7 @@ public class DownloadTracker { ...@@ -230,7 +235,7 @@ public class DownloadTracker {
downloadHelper.addTrackSelectionForSingleRenderer( downloadHelper.addTrackSelectionForSingleRenderer(
periodIndex, periodIndex,
/* rendererIndex= */ i, /* rendererIndex= */ i,
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters,
trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
} }
} }
......
/*
* 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);
}
...@@ -19,19 +19,18 @@ import android.app.Dialog; ...@@ -19,19 +19,18 @@ import android.app.Dialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.material.tabs.TabLayout; import androidx.appcompat.app.AppCompatDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import androidx.appcompat.app.AppCompatDialog;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
...@@ -39,6 +38,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Selecti ...@@ -39,6 +38,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Selecti
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.ui.TrackSelectionView; import com.google.android.exoplayer2.ui.TrackSelectionView;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
......
...@@ -23,4 +23,8 @@ ...@@ -23,4 +23,8 @@
android:title="@string/random_abr" android:title="@string/random_abr"
android:checkable="true" android:checkable="true"
app:showAsAction="never"/> app:showAsAction="never"/>
<item android:id="@+id/tunneling"
android:title="@string/tunneling"
android:checkable="true"
app:showAsAction="never"/>
</menu> </menu>
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string> <string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string> <string name="error_drm_unsupported_before_api_18">Protected content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string> <string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
...@@ -53,6 +53,8 @@ ...@@ -53,6 +53,8 @@
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string> <string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
<string name="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
<string name="download_start_error">Failed to start download</string> <string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string> <string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
...@@ -61,10 +63,14 @@ ...@@ -61,10 +63,14 @@
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string> <string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
<string name="download_live_unsupported">This demo app does not support downloading live content</string>
<string name="download_ads_unsupported">IMA does not support offline ads</string> <string name="download_ads_unsupported">IMA does not support offline ads</string>
<string name="prefer_extension_decoders">Prefer extension decoders</string> <string name="prefer_extension_decoders">Prefer extension decoders</string>
<string name="random_abr">Enable random ABR</string> <string name="random_abr">Enable random ABR</string>
<string name="tunneling">Request multimedia tunneling</string>
</resources> </resources>
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
</style> </style>
<style name="PlayerTheme.Spherical"> <style name="PlayerTheme.Spherical">
<item name="surface_type">spherical_view</item> <item name="surface_type">spherical_gl_surface_view</item>
</style> </style>
</resources> </resources>
# ExoPlayer SurfaceControl demo
This app demonstrates how to use the [SurfaceControl][] API to redirect video
output from ExoPlayer between different views or off-screen. `SurfaceControl`
is new in Android 10, so the app requires `minSdkVersion` 29.
The app layout has a grid of `SurfaceViews`. Initially video is output to one
of the views. Tap a `SurfaceView` to move video output to it. You can also tap
the buttons at the top of the activity to move video output off-screen, to a
full-screen `SurfaceView` or to a new activity.
When using `SurfaceControl`, the `MediaCodec` always has the same surface
attached to it, which can be freely 'reparented' to any `SurfaceView` (or
off-screen) without any interruptions to playback. This works better than
calling `MediaCodec.setOutputSurface` to change the output surface of the codec
because `MediaCodec` does not re-render its last frame when that method is
called, and because you can move output off-screen easily (`setOutputSurface`
can't take a `null` surface, so the player has to use a `DummySurface`, which
doesn't handle protected output on all devices).
[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl
// Copyright (C) 2017 The Android Open Source Project // Copyright (C) 2019 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.
...@@ -25,8 +25,8 @@ android { ...@@ -25,8 +25,8 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion 29
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
...@@ -35,14 +35,11 @@ android { ...@@ -35,14 +35,11 @@ android {
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt') proguardFiles getDefaultProguardFile('proguard-android.txt')
} }
debug {
jniDebuggable = true
}
} }
lintOptions { lintOptions {
// The demo app isn't indexed and doesn't have translations. // This demo app does not have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation' disable 'MissingTranslation'
} }
} }
...@@ -50,10 +47,5 @@ dependencies { ...@@ -50,10 +47,5 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-ima')
implementation 'androidx.annotation:annotation:1.1.0'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project <!-- Copyright (C) 2019 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.
...@@ -14,25 +14,28 @@ ...@@ -14,25 +14,28 @@
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.imademo"> package="com.google.android.exoplayer2.surfacedemo">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/> <uses-sdk/>
<uses-permission android:name="android.permission.INTERNET"/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
android:largeHeap="true" android:allowBackup="false"> <application
android:allowBackup="false"
<activity android:name="com.google.android.exoplayer2.imademo.MainActivity" android:icon="@mipmap/ic_launcher"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:label="@string/application_name">
android:label="@string/application_name" <activity android:name=".MainActivity">
android:theme="@style/PlayerTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="com.google.android.exoplayer.surfacedemo.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> </activity>
</application> </application>
</manifest> </manifest>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<GridLayout
android:id="@+id/grid_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:columnCount="3"/>
<com.google.android.exoplayer2.ui.PlayerControlView
android:id="@+id/player_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:show_timeout="0"/>
</LinearLayout>
<SurfaceView
android:id="@+id/full_screen_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project <!-- Copyright (C) 2019 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.
...@@ -14,5 +14,10 @@ ...@@ -14,5 +14,10 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<style name="VrTheme" parent="android:Theme.Material"/>
<string name="application_name">ExoPlayer SurfaceControl demo</string>
<string name="no_output_label">No output</string>
<string name="full_screen_label">Full screen</string>
<string name="new_activity_label">New activity</string>
</resources> </resources>
# ExoPlayer AV1 extension #
The AV1 extension provides `Libgav1VideoRenderer`, which uses libgav1 native
library to decode AV1 videos.
## License note ##
Please note that whilst the code in this repository is licensed under
[Apache 2.0][], using this extension also requires building and including one or
more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
In addition, it's necessary to fetch cpu_features library and libgav1 with its
dependencies as follows:
* Set the following environment variables:
```
cd "<path to exoplayer checkout>"
EXOPLAYER_ROOT="$(pwd)"
AV1_EXT_PATH="${EXOPLAYER_ROOT}/extensions/av1/src/main"
```
* Fetch cpu_features library:
```
cd "${AV1_EXT_PATH}/jni" && \
git clone https://github.com/google/cpu_features
```
* Fetch libgav1:
```
cd "${AV1_EXT_PATH}/jni" && \
git clone https://chromium.googlesource.com/codecs/libgav1 libgav1
```
* Fetch Abseil:
```
cd "${AV1_EXT_PATH}/jni/libgav1" && \
git clone https://github.com/abseil/abseil-cpp.git third_party/abseil-cpp
```
* [Install CMake][].
Having followed these steps, gradle will build the extension automatically when
run on the command line or via Android Studio, using [CMake][] and [Ninja][]
to configure and build libgav1 and the extension's [JNI wrapper library][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Install CMake]: https://developer.android.com/studio/projects/install-ndk
[CMake]: https://cmake.org/
[Ninja]: https://ninja-build.org
[JNI wrapper library]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/av1/src/main/jni/gav1_jni.cc
## Build instructions (Windows) ##
We do not provide support for building this extension on Windows, however it
should be possible to follow the Linux instructions in [Windows PowerShell][].
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
## Using the extension ##
Once you've followed the instructions above to check out, build and depend on
the extension, the next step is to tell ExoPlayer to use `Libgav1VideoRenderer`.
How you do this depends on which player API you're using:
* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
you can enable using the extension by setting the `extensionRendererMode`
parameter of the `DefaultRenderersFactory` constructor to
`EXTENSION_RENDERER_MODE_ON`. This will use `Libgav1VideoRenderer` for
playback if `MediaCodecVideoRenderer` doesn't support decoding the input AV1
stream. Pass `EXTENSION_RENDERER_MODE_PREFER` to give `Libgav1VideoRenderer`
priority over `MediaCodecVideoRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add a `Libvgav1VideoRenderer`
to the output list in `buildVideoRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return a
`Libgav1VideoRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format.
* If you're using `ExoPlayer.Builder`, pass a `Libgav1VideoRenderer` in the
array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation.
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.
## Rendering options ##
There are two possibilities for rendering the output `Libgav1VideoRenderer`
gets from the libgav1 decoder:
* GL rendering using GL shader for color space conversion
* If you are using `SimpleExoPlayer` with `PlayerView`, enable this option by
setting `surface_type` of `PlayerView` to be
`video_decoder_gl_surface_view`.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message
of type `C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER` with an instance of
`VideoDecoderOutputBufferRenderer` as its object.
* Native rendering using `ANativeWindow`
* If you are using `SimpleExoPlayer` with `PlayerView`, this option is enabled
by default.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of
type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object.
Note: Although the default option uses `ANativeWindow`, based on our testing the
GL rendering mode has better performance, so should be preferred
## Links ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.av1.*`
belong to this module.
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
// Copyright (C) 2018 The Android Open Source Project // Copyright (C) 2019 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.
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../constants.gradle' apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
...@@ -25,21 +25,49 @@ android { ...@@ -25,21 +25,49 @@ android {
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
externalNativeBuild {
cmake {
// Debug CMake build type causes video frames to drop,
// so native library should always use Release build type.
arguments "-DCMAKE_BUILD_TYPE=Release"
targets "gav1JNI"
}
}
}
// This option resolves the problem of finding libgav1JNI.so
// on multiple paths. The first one found is picked.
packagingOptions {
pickFirst 'lib/arm64-v8a/libgav1JNI.so'
pickFirst 'lib/armeabi-v7a/libgav1JNI.so'
pickFirst 'lib/x86/libgav1JNI.so'
pickFirst 'lib/x86_64/libgav1JNI.so'
} }
lintOptions { sourceSets.main {
// Robolectric depends on BouncyCastle, which depends on javax.naming, // As native JNI library build is invoked from gradle, this is
// which is not part of Android. // not needed. However, it exposes the built library and keeps
disable 'InvalidPackage' // consistency with the other extensions.
jniLibs.srcDir 'src/main/libs'
} }
}
testOptions.unitTests.includeAndroidResources = true // Configure the native build only if libgav1 is present, to avoid gradle sync
// failures if libgav1 hasn't been checked out according to the README and CMake
// isn't installed.
if (project.file('src/main/jni/libgav1').exists()) {
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
android.externalNativeBuild.cmake.version = '3.7.1+'
} }
dependencies { dependencies {
api 'androidx.test:core:' + androidXTestVersion
api 'org.robolectric:robolectric:' + robolectricVersion
api project(modulePrefix + 'testutils')
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
}
ext {
javadocTitle = 'AV1 extension'
} }
apply from: '../../javadoc_library.gradle'
# Proguard rules specific to the AV1 extension.
# This prevents the names of native methods from being obfuscated.
-keepclasseswithmembernames class * {
native <methods>;
}
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project <!-- Copyright (C) 2019 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.
...@@ -14,4 +14,4 @@ ...@@ -14,4 +14,4 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.testutil"/> <manifest package="com.google.android.exoplayer2.ext.av1"/>
/*
* 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.ext.av1;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import java.nio.ByteBuffer;
/** Gav1 decoder. */
/* package */ final class Gav1Decoder
extends SimpleDecoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, Gav1DecoderException> {
// LINT.IfChange
private static final int GAV1_ERROR = 0;
private static final int GAV1_OK = 1;
private static final int GAV1_DECODE_ONLY = 2;
// LINT.ThenChange(../../../../../../../jni/gav1_jni.cc)
private final long gav1DecoderContext;
@C.VideoOutputMode private volatile int outputMode;
/**
* Creates a Gav1Decoder.
*
* @param numInputBuffers Number of input buffers.
* @param numOutputBuffers Number of output buffers.
* @param initialInputBufferSize The initial size of each input buffer, in bytes.
* @param threads Number of threads libgav1 will use to decode.
* @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder.
*/
public Gav1Decoder(
int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, int threads)
throws Gav1DecoderException {
super(
new VideoDecoderInputBuffer[numInputBuffers],
new VideoDecoderOutputBuffer[numOutputBuffers]);
if (!Gav1Library.isAvailable()) {
throw new Gav1DecoderException("Failed to load decoder native library.");
}
gav1DecoderContext = gav1Init(threads);
if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) {
throw new Gav1DecoderException(
"Failed to initialize decoder. Error: " + gav1GetErrorMessage(gav1DecoderContext));
}
setInitialInputBufferSize(initialInputBufferSize);
}
@Override
public String getName() {
return "libgav1";
}
/**
* Sets the output mode for frames rendered by the decoder.
*
* @param outputMode The output mode.
*/
public void setOutputMode(@C.VideoOutputMode int outputMode) {
this.outputMode = outputMode;
}
@Override
protected VideoDecoderInputBuffer createInputBuffer() {
return new VideoDecoderInputBuffer();
}
@Override
protected VideoDecoderOutputBuffer createOutputBuffer() {
return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
}
@Nullable
@Override
protected Gav1DecoderException decode(
VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
int inputSize = inputData.limit();
if (gav1Decode(gav1DecoderContext, inputData, inputSize) == GAV1_ERROR) {
return new Gav1DecoderException(
"gav1Decode error: " + gav1GetErrorMessage(gav1DecoderContext));
}
boolean decodeOnly = inputBuffer.isDecodeOnly();
if (!decodeOnly) {
outputBuffer.init(inputBuffer.timeUs, outputMode, /* supplementalData= */ null);
}
// We need to dequeue the decoded frame from the decoder even when the input data is
// decode-only.
int getFrameResult = gav1GetFrame(gav1DecoderContext, outputBuffer, decodeOnly);
if (getFrameResult == GAV1_ERROR) {
return new Gav1DecoderException(
"gav1GetFrame error: " + gav1GetErrorMessage(gav1DecoderContext));
}
if (getFrameResult == GAV1_DECODE_ONLY) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
if (!decodeOnly) {
outputBuffer.colorInfo = inputBuffer.colorInfo;
}
return null;
}
@Override
protected Gav1DecoderException createUnexpectedDecodeException(Throwable error) {
return new Gav1DecoderException("Unexpected decode error", error);
}
@Override
public void release() {
super.release();
gav1Close(gav1DecoderContext);
}
@Override
protected void releaseOutputBuffer(VideoDecoderOutputBuffer buffer) {
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
// require a call to gav1ReleaseFrame.
if (buffer.mode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
gav1ReleaseFrame(gav1DecoderContext, buffer);
}
super.releaseOutputBuffer(buffer);
}
/**
* Renders output buffer to the given surface. Must only be called when in {@link
* C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
*
* @param outputBuffer Output buffer.
* @param surface Output surface.
* @throws Gav1DecoderException Thrown if called with invalid output mode or frame rendering
* fails.
*/
public void renderToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws Gav1DecoderException {
if (outputBuffer.mode != C.VIDEO_OUTPUT_MODE_SURFACE_YUV) {
throw new Gav1DecoderException("Invalid output mode.");
}
if (gav1RenderFrame(gav1DecoderContext, surface, outputBuffer) == GAV1_ERROR) {
throw new Gav1DecoderException(
"Buffer render error: " + gav1GetErrorMessage(gav1DecoderContext));
}
}
/**
* Initializes a libgav1 decoder.
*
* @param threads Number of threads to be used by a libgav1 decoder.
* @return The address of the decoder context or {@link #GAV1_ERROR} if there was an error.
*/
private native long gav1Init(int threads);
/**
* Deallocates the decoder context.
*
* @param context Decoder context.
*/
private native void gav1Close(long context);
/**
* Decodes the encoded data passed.
*
* @param context Decoder context.
* @param encodedData Encoded data.
* @param length Length of the data buffer.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1Decode(long context, ByteBuffer encodedData, int length);
/**
* Gets the decoded frame.
*
* @param context Decoder context.
* @param outputBuffer Output buffer for the decoded frame.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_DECODE_ONLY} if successful but the frame
* is decode-only, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1GetFrame(
long context, VideoDecoderOutputBuffer outputBuffer, boolean decodeOnly);
/**
* Renders the frame to the surface. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
*
* @param context Decoder context.
* @param surface Output surface.
* @param outputBuffer Output buffer with the decoded frame.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occured.
*/
private native int gav1RenderFrame(
long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
/**
* Releases the frame. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
*
* @param context Decoder context.
* @param outputBuffer Output buffer.
*/
private native void gav1ReleaseFrame(long context, VideoDecoderOutputBuffer outputBuffer);
/**
* Returns a human-readable string describing the last error encountered in the given context.
*
* @param context Decoder context.
* @return A string describing the last encountered error.
*/
private native String gav1GetErrorMessage(long context);
/**
* Returns whether an error occured.
*
* @param context Decoder context.
* @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured.
*/
private native int gav1CheckError(long context);
}
/*
* 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.ext.av1;
import com.google.android.exoplayer2.video.VideoDecoderException;
/** Thrown when a libgav1 decoder error occurs. */
public final class Gav1DecoderException extends VideoDecoderException {
/* package */ Gav1DecoderException(String message) {
super(message);
}
/* package */ Gav1DecoderException(String message, Throwable cause) {
super(message, cause);
}
}
/*
* 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.ext.av1;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
/** Configures and queries the underlying native library. */
public final class Gav1Library {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.gav1");
}
private static final LibraryLoader LOADER = new LibraryLoader("gav1JNI");
private Gav1Library() {}
/** Returns whether the underlying library is available, loading it if necessary. */
public static boolean isAvailable() {
return LOADER.isAvailable();
}
}
/*
* 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.ext.av1;
import static java.lang.Runtime.getRuntime;
import android.os.Handler;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;
import com.google.android.exoplayer2.video.VideoDecoderException;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
/**
* Decodes and renders video using libgav1 decoder.
*
* <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
* on the playback thread:
*
* <ul>
* <li>Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload
* should be the target {@link Surface}, or null.
* <li>Message with type {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output
* buffer renderer. The message payload should be the target {@link
* VideoDecoderOutputBufferRenderer}, or null.
* </ul>
*/
public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4;
private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4;
/* Default size based on 720p resolution video compressed by a factor of two. */
private static final int DEFAULT_INPUT_BUFFER_SIZE =
Util.ceilDivide(1280, 64) * Util.ceilDivide(720, 64) * (64 * 64 * 3 / 2) / 2;
/** The number of input buffers. */
private final int numInputBuffers;
/**
* The number of output buffers. The renderer may limit the minimum possible value due to
* requiring multiple output buffers to be dequeued at a time for it to make progress.
*/
private final int numOutputBuffers;
private final int threads;
@Nullable private Gav1Decoder decoder;
/**
* Creates a Libgav1VideoRenderer.
*
* @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 Libgav1VideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* threads= */ getRuntime().availableProcessors(),
DEFAULT_NUM_OF_INPUT_BUFFERS,
DEFAULT_NUM_OF_OUTPUT_BUFFERS);
}
/**
* Creates a Libgav1VideoRenderer.
*
* @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)}.
* @param threads Number of threads libgav1 will use to decode.
* @param numInputBuffers Number of input buffers.
* @param numOutputBuffers Number of output buffers.
*/
public Libgav1VideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
int threads,
int numInputBuffers,
int numOutputBuffers) {
super(
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false);
this.threads = threads;
this.numInputBuffers = numInputBuffers;
this.numOutputBuffers = numOutputBuffers;
}
@Override
@Capabilities
protected int supportsFormatInternal(
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|| !Gav1Library.isAvailable()) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
}
if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
}
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
}
@Override
protected SimpleDecoder<
VideoDecoderInputBuffer,
? extends VideoDecoderOutputBuffer,
? extends VideoDecoderException>
createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws VideoDecoderException {
TraceUtil.beginSection("createGav1Decoder");
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
Gav1Decoder decoder =
new Gav1Decoder(numInputBuffers, numOutputBuffers, initialInputBufferSize, threads);
this.decoder = decoder;
TraceUtil.endSection();
return decoder;
}
@Override
protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws Gav1DecoderException {
if (decoder == null) {
throw new Gav1DecoderException(
"Failed to render output buffer to surface: decoder is not initialized.");
}
decoder.renderToSurface(outputBuffer, surface);
outputBuffer.release();
}
@Override
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
if (decoder != null) {
decoder.setOutputMode(outputMode);
}
}
// PlayerMessage.Target implementation.
@Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
if (messageType == C.MSG_SET_SURFACE) {
setOutputSurface((Surface) message);
} else if (messageType == C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) {
setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message);
} else {
super.handleMessage(messageType, message);
}
}
}
/*
* 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.
*/
@NonNullApi
package com.google.android.exoplayer2.ext.av1;
import com.google.android.exoplayer2.util.NonNullApi;
# libgav1JNI requires modern CMake.
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
# libgav1JNI requires C++11.
set(CMAKE_CXX_STANDARD 11)
project(libgav1JNI C CXX)
# Devices using armeabi-v7a are not required to support
# Neon which is why Neon is disabled by default for
# armeabi-v7a build. This flag enables it.
if(${ANDROID_ABI} MATCHES "armeabi-v7a")
add_compile_options("-mfpu=neon")
add_compile_options("-fPIC")
endif()
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
set(libgav1_jni_output_directory
${libgav1_jni_root}/../libs/${ANDROID_ABI}/)
set(libgav1_root "${libgav1_jni_root}/libgav1")
set(libgav1_build "${libgav1_jni_build}/libgav1")
set(cpu_features_root "${libgav1_jni_root}/cpu_features")
set(cpu_features_build "${libgav1_jni_build}/cpu_features")
# Build cpu_features library.
add_subdirectory("${cpu_features_root}"
"${cpu_features_build}"
EXCLUDE_FROM_ALL)
# Build libgav1.
add_subdirectory("${libgav1_root}"
"${libgav1_build}"
EXCLUDE_FROM_ALL)
# Build libgav1JNI.
add_library(gav1JNI
SHARED
gav1_jni.cc)
# Locate NDK log library.
find_library(android_log_lib log)
# Link libgav1JNI against used libraries.
target_link_libraries(gav1JNI
PRIVATE android
PRIVATE cpu_features
PRIVATE libgav1_static
PRIVATE ${android_log_lib})
# Specify output directory for libgav1JNI.
set_target_properties(gav1JNI PROPERTIES
LIBRARY_OUTPUT_DIRECTORY
${libgav1_jni_output_directory})
...@@ -32,12 +32,13 @@ android { ...@@ -32,12 +32,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:17.0.0'
implementation 'androidx.annotation:annotation:1.1.0' 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
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -15,9 +15,9 @@ ...@@ -15,9 +15,9 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import androidx.annotation.Nullable;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import java.util.Arrays; import java.util.Arrays;
...@@ -39,9 +39,14 @@ import java.util.Arrays; ...@@ -39,9 +39,14 @@ import java.util.Arrays;
* The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown. * The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
*/ */
public final long defaultPositionUs; public final long defaultPositionUs;
/** Whether the item is live content, or {@code false} if unknown. */
public final boolean isLive;
private ItemData() { private ItemData() {
this(/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */ C.TIME_UNSET); this(
/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */
C.TIME_UNSET,
/* isLive= */ false);
} }
/** /**
...@@ -49,26 +54,29 @@ import java.util.Arrays; ...@@ -49,26 +54,29 @@ import java.util.Arrays;
* *
* @param durationUs See {@link #durationsUs}. * @param durationUs See {@link #durationsUs}.
* @param defaultPositionUs See {@link #defaultPositionUs}. * @param defaultPositionUs See {@link #defaultPositionUs}.
* @param isLive See {@link #isLive}.
*/ */
public ItemData(long durationUs, long defaultPositionUs) { public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
this.durationUs = durationUs; this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs; this.defaultPositionUs = defaultPositionUs;
this.isLive = isLive;
} }
/** Returns an instance with the given {@link #durationsUs}. */ /**
public ItemData copyWithDurationUs(long durationUs) { * Returns a copy of this instance with the given values.
if (durationUs == this.durationUs) { *
return this; * @param durationUs The duration in microseconds, or {@link C#TIME_UNSET} if unknown.
} * @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
return new ItemData(durationUs, defaultPositionUs); * if unknown.
} * @param isLive Whether the item is live, or {@code false} if unknown.
*/
/** Returns an instance with the given {@link #defaultPositionsUs}. */ public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
public ItemData copyWithDefaultPositionUs(long defaultPositionUs) { if (durationUs == this.durationUs
if (defaultPositionUs == this.defaultPositionUs) { && defaultPositionUs == this.defaultPositionUs
&& isLive == this.isLive) {
return this; return this;
} }
return new ItemData(durationUs, defaultPositionUs); return new ItemData(durationUs, defaultPositionUs, isLive);
} }
} }
...@@ -80,6 +88,7 @@ import java.util.Arrays; ...@@ -80,6 +88,7 @@ import java.util.Arrays;
private final int[] ids; private final int[] ids;
private final long[] durationsUs; private final long[] durationsUs;
private final long[] defaultPositionsUs; private final long[] defaultPositionsUs;
private final boolean[] isLive;
/** /**
* Creates a Cast timeline from the given data. * Creates a Cast timeline from the given data.
...@@ -93,12 +102,14 @@ import java.util.Arrays; ...@@ -93,12 +102,14 @@ import java.util.Arrays;
ids = Arrays.copyOf(itemIds, itemCount); ids = Arrays.copyOf(itemIds, itemCount);
durationsUs = new long[itemCount]; durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount]; defaultPositionsUs = new long[itemCount];
isLive = new boolean[itemCount];
for (int i = 0; i < ids.length; i++) { for (int i = 0; i < ids.length; i++) {
int id = ids[i]; int id = ids[i];
idsToIndex.put(id, i); idsToIndex.put(id, i);
ItemData data = itemIdToData.get(id, ItemData.EMPTY); ItemData data = itemIdToData.get(id, ItemData.EMPTY);
durationsUs[i] = data.durationUs; durationsUs[i] = data.durationUs;
defaultPositionsUs[i] = data.defaultPositionUs; defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
isLive[i] = data.isLive;
} }
} }
...@@ -110,17 +121,18 @@ import java.util.Arrays; ...@@ -110,17 +121,18 @@ import java.util.Arrays;
} }
@Override @Override
public Window getWindow( public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex]; long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET; boolean isDynamic = durationUs == C.TIME_UNSET;
Object tag = setTag ? ids[windowIndex] : null;
return window.set( return window.set(
tag, /* uid= */ ids[windowIndex],
/* tag= */ ids[windowIndex],
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic, /* isSeekable= */ !isDynamic,
isDynamic, isDynamic,
isLive[windowIndex],
defaultPositionsUs[windowIndex], defaultPositionsUs[windowIndex],
durationUs, durationUs,
/* firstPeriodIndex= */ windowIndex, /* firstPeriodIndex= */ windowIndex,
...@@ -161,7 +173,8 @@ import java.util.Arrays; ...@@ -161,7 +173,8 @@ import java.util.Arrays;
CastTimeline that = (CastTimeline) other; CastTimeline that = (CastTimeline) other;
return Arrays.equals(ids, that.ids) return Arrays.equals(ids, that.ids)
&& Arrays.equals(durationsUs, that.durationsUs) && Arrays.equals(durationsUs, that.durationsUs)
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs); && Arrays.equals(defaultPositionsUs, that.defaultPositionsUs)
&& Arrays.equals(isLive, that.isLive);
} }
@Override @Override
...@@ -169,6 +182,7 @@ import java.util.Arrays; ...@@ -169,6 +182,7 @@ import java.util.Arrays;
int result = Arrays.hashCode(ids); int result = Arrays.hashCode(ids);
result = 31 * result + Arrays.hashCode(durationsUs); result = 31 * result + Arrays.hashCode(durationsUs);
result = 31 * result + Arrays.hashCode(defaultPositionsUs); result = 31 * result + Arrays.hashCode(defaultPositionsUs);
result = 31 * result + Arrays.hashCode(isLive);
return result; return result;
} }
......
...@@ -16,7 +16,9 @@ ...@@ -16,7 +16,9 @@
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem; 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.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient;
...@@ -61,25 +63,33 @@ import java.util.HashSet; ...@@ -61,25 +63,33 @@ import java.util.HashSet;
} }
int currentItemId = mediaStatus.getCurrentItemId(); int currentItemId = mediaStatus.getCurrentItemId();
long durationUs = CastUtils.getStreamDurationUs(mediaStatus.getMediaInfo()); updateItemData(
itemIdToData.put( currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
currentItemId,
itemIdToData
.get(currentItemId, CastTimeline.ItemData.EMPTY)
.copyWithDurationUs(durationUs));
for (MediaQueueItem item : mediaStatus.getQueueItems()) { for (MediaQueueItem item : mediaStatus.getQueueItems()) {
int itemId = item.getItemId(); long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
itemIdToData.put( updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
itemId,
itemIdToData
.get(itemId, CastTimeline.ItemData.EMPTY)
.copyWithDefaultPositionUs((long) (item.getStartTime() * C.MICROS_PER_SECOND)));
} }
return new CastTimeline(itemIds, itemIdToData); return new CastTimeline(itemIds, itemIdToData);
} }
private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
if (durationUs == C.TIME_UNSET) {
durationUs = previousData.durationUs;
}
boolean isLive =
mediaInfo == null
? previousData.isLive
: mediaInfo.getStreamType() == MediaInfo.STREAM_TYPE_LIVE;
if (defaultPositionUs == C.TIME_UNSET) {
defaultPositionUs = previousData.defaultPositionUs;
}
itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
}
private void removeUnusedItemDataEntries(int[] itemIds) { private void removeUnusedItemDataEntries(int[] itemIds) {
HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2); HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
for (int id : itemIds) { for (int id : itemIds) {
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.gms.cast.CastStatusCodes; import com.google.android.gms.cast.CastStatusCodes;
...@@ -33,7 +34,7 @@ import com.google.android.gms.cast.MediaTrack; ...@@ -33,7 +34,7 @@ import com.google.android.gms.cast.MediaTrack;
* @param mediaInfo The media info to get the duration from. * @param mediaInfo The media info to get the duration from.
* @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable. * @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
*/ */
public static long getStreamDurationUs(MediaInfo mediaInfo) { public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) {
if (mediaInfo == null) { if (mediaInfo == null) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
......
...@@ -28,11 +28,33 @@ import java.util.List; ...@@ -28,11 +28,33 @@ import java.util.List;
*/ */
public final class DefaultCastOptionsProvider implements OptionsProvider { public final class DefaultCastOptionsProvider implements OptionsProvider {
/**
* App id of the Default Media Receiver app. Apps that do not require DRM support may use this
* receiver receiver app ID.
*
* <p>See https://developers.google.com/cast/docs/caf_receiver/#default_media_receiver.
*/
public static final String APP_ID_DEFAULT_RECEIVER =
CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
/**
* App id for receiver app with rudimentary support for DRM.
*
* <p>This app id is only suitable for ExoPlayer's Cast Demo app, and it is not intended for
* production use. In order to use DRM, custom receiver apps should be used. For environments that
* do not require DRM, the default receiver app should be used (see {@link
* #APP_ID_DEFAULT_RECEIVER}).
*/
// TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref:
// b/128603245].
public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
@Override @Override
public CastOptions getCastOptions(Context context) { public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder() return new CastOptions.Builder()
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM)
.setStopReceiverApplicationWhenEndingSession(true).build(); .setStopReceiverApplicationWhenEndingSession(true)
.build();
} }
@Override @Override
......
/*
* 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.ext.cast;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaQueueItem;
import java.util.HashMap;
import java.util.Iterator;
import java.util.UUID;
import org.json.JSONException;
import org.json.JSONObject;
/** Default {@link MediaItemConverter} implementation. */
public final class DefaultMediaItemConverter implements MediaItemConverter {
private static final String KEY_MEDIA_ITEM = "mediaItem";
private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig";
private static final String KEY_URI = "uri";
private static final String KEY_TITLE = "title";
private static final String KEY_MIME_TYPE = "mimeType";
private static final String KEY_DRM_CONFIGURATION = "drmConfiguration";
private static final String KEY_UUID = "uuid";
private static final String KEY_LICENSE_URI = "licenseUri";
private static final String KEY_REQUEST_HEADERS = "requestHeaders";
@Override
public MediaItem toMediaItem(MediaQueueItem item) {
return getMediaItem(item.getMedia().getCustomData());
}
@Override
public MediaQueueItem toMediaQueueItem(MediaItem item) {
if (item.mimeType == null) {
throw new IllegalArgumentException("The item must specify its mimeType");
}
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
if (item.title != null) {
metadata.putString(MediaMetadata.KEY_TITLE, item.title);
}
MediaInfo mediaInfo =
new MediaInfo.Builder(item.uri.toString())
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(item.mimeType)
.setMetadata(metadata)
.setCustomData(getCustomData(item))
.build();
return new MediaQueueItem.Builder(mediaInfo).build();
}
// Deserialization.
private static MediaItem getMediaItem(JSONObject customData) {
try {
JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM);
MediaItem.Builder builder = new MediaItem.Builder();
builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
if (mediaItemJson.has(KEY_TITLE)) {
builder.setTitle(mediaItemJson.getString(KEY_TITLE));
}
if (mediaItemJson.has(KEY_MIME_TYPE)) {
builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
}
if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) {
builder.setDrmConfiguration(
getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION)));
}
return builder.build();
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException {
UUID uuid = UUID.fromString(json.getString(KEY_UUID));
Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI));
JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS);
HashMap<String, String> requestHeaders = new HashMap<>();
for (Iterator<String> iterator = requestHeadersJson.keys(); iterator.hasNext(); ) {
String key = iterator.next();
requestHeaders.put(key, requestHeadersJson.getString(key));
}
return new DrmConfiguration(uuid, licenseUri, requestHeaders);
}
// Serialization.
private static JSONObject getCustomData(MediaItem item) {
JSONObject json = new JSONObject();
try {
json.put(KEY_MEDIA_ITEM, getMediaItemJson(item));
JSONObject playerConfigJson = getPlayerConfigJson(item);
if (playerConfigJson != null) {
json.put(KEY_PLAYER_CONFIG, playerConfigJson);
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
return json;
}
private static JSONObject getMediaItemJson(MediaItem item) throws JSONException {
JSONObject json = new JSONObject();
json.put(KEY_URI, item.uri.toString());
json.put(KEY_TITLE, item.title);
json.put(KEY_MIME_TYPE, item.mimeType);
if (item.drmConfiguration != null) {
json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration));
}
return json;
}
private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration)
throws JSONException {
JSONObject json = new JSONObject();
json.put(KEY_UUID, drmConfiguration.uuid);
json.put(KEY_LICENSE_URI, drmConfiguration.licenseUri);
json.put(KEY_REQUEST_HEADERS, new JSONObject(drmConfiguration.requestHeaders));
return json;
}
@Nullable
private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException {
DrmConfiguration drmConfiguration = item.drmConfiguration;
if (drmConfiguration == null) {
return null;
}
String drmScheme;
if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) {
drmScheme = "widevine";
} else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) {
drmScheme = "playready";
} else {
return null;
}
JSONObject exoPlayerConfigJson = new JSONObject();
exoPlayerConfigJson.put("withCredentials", false);
exoPlayerConfigJson.put("protectionSystem", drmScheme);
if (drmConfiguration.licenseUri != null) {
exoPlayerConfigJson.put("licenseUrl", drmConfiguration.licenseUri);
}
if (!drmConfiguration.requestHeaders.isEmpty()) {
exoPlayerConfigJson.put("headers", new JSONObject(drmConfiguration.requestHeaders));
}
return exoPlayerConfigJson;
}
}
/*
* 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.ext.cast;
import com.google.android.gms.cast.MediaQueueItem;
/** Converts between {@link MediaItem} and the Cast SDK's {@link MediaQueueItem}. */
public interface MediaItemConverter {
/**
* Converts a {@link MediaItem} to a {@link MediaQueueItem}.
*
* @param mediaItem The {@link MediaItem}.
* @return An equivalent {@link MediaQueueItem}.
*/
MediaQueueItem toMediaQueueItem(MediaItem mediaItem);
/**
* Converts a {@link MediaQueueItem} to a {@link MediaItem}.
*
* @param mediaQueueItem The {@link MediaQueueItem}.
* @return The equivalent {@link MediaItem}.
*/
MediaItem toMediaItem(MediaQueueItem mediaQueueItem);
}
/*
* 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;
/** Represents a sequence of {@link MediaItem MediaItems}. */
public interface MediaItemQueue {
/**
* Returns the item at the given index.
*
* @param index The index of the item to retrieve.
* @return The item at the given index.
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
*/
MediaItem get(int index);
/** Returns the number of items in this queue. */
int getSize();
/**
* Appends the given sequence of items to the queue.
*
* @param items The sequence of items to append.
*/
void add(MediaItem... items);
/**
* Adds the given sequence of items to the queue at the given position, so that the first of
* {@code items} is placed at the given index.
*
* @param index The index at which {@code items} will be inserted.
* @param items The sequence of items to append.
* @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}.
*/
void add(int index, MediaItem... items);
/**
* Moves an existing item within the playlist.
*
* <p>Calling this method is equivalent to removing the item at position {@code indexFrom} and
* immediately inserting it at position {@code indexTo}. If the moved item is being played at the
* moment of the invocation, playback will stick with the moved item.
*
* @param indexFrom The index of the item to move.
* @param indexTo The index at which the item will be placed after this operation.
* @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}.
*/
void move(int indexFrom, int indexTo);
/**
* Removes an item from the queue.
*
* @param index The index of the item to remove from the queue.
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
*/
void remove(int index);
/**
* Removes a range of items from the queue.
*
* <p>Does nothing if an empty range ({@code from == exclusiveTo}) is passed.
*
* @param from The inclusive index at which the range to remove starts.
* @param exclusiveTo The exclusive index at which the range to remove ends.
* @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from >
* exclusiveTo}.
*/
void removeRange(int from, int exclusiveTo);
/** Removes all items in the queue. */
void clear();
}
/*
* 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.
*/
@NonNullApi
package com.google.android.exoplayer2.ext.cast;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -14,4 +14,6 @@ ...@@ -14,4 +14,6 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.ext.cast.test"/> <manifest package="com.google.android.exoplayer2.ext.cast.test">
<uses-sdk/>
</manifest>
/*
* 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.ext.cast;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Player;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.SessionManager;
import com.google.android.gms.cast.framework.media.MediaQueue;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
/** Tests for {@link CastPlayer}. */
@RunWith(AndroidJUnit4.class)
public class CastPlayerTest {
private CastPlayer castPlayer;
private RemoteMediaClient.Listener remoteMediaClientListener;
@Mock private RemoteMediaClient mockRemoteMediaClient;
@Mock private MediaStatus mockMediaStatus;
@Mock private MediaQueue mockMediaQueue;
@Mock private CastContext mockCastContext;
@Mock private SessionManager mockSessionManager;
@Mock private CastSession mockCastSession;
@Mock private Player.EventListener mockListener;
@Mock private PendingResult<RemoteMediaClient.MediaChannelResult> mockPendingResult;
@Captor
private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>>
setResultCallbackArgumentCaptor;
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor;
@Before
public void setUp() {
initMocks(this);
when(mockCastContext.getSessionManager()).thenReturn(mockSessionManager);
when(mockSessionManager.getCurrentCastSession()).thenReturn(mockCastSession);
when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient);
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
when(mockMediaQueue.getItemIds()).thenReturn(new int[0]);
// Make the remote media client present the same default values as ExoPlayer:
when(mockRemoteMediaClient.isPaused()).thenReturn(true);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
castPlayer = new CastPlayer(mockCastContext);
castPlayer.addListener(mockListener);
verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture());
remoteMediaClientListener = listenerArgumentCaptor.getValue();
}
@Test
public void testSetPlayWhenReady_masksRemoteState() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
castPlayer.setPlayWhenReady(true);
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
// There is a status update in the middle, which should be hidden by masking.
remoteMediaClientListener.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the remoteMediaClient has updated its state according to the play() call.
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verifyNoMoreInteractions(mockListener);
}
@Test
public void testSetPlayWhenReadyMasking_updatesUponResultChange() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
castPlayer.setPlayWhenReady(true);
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
// Upon result, the remote media client is still paused. The state should reflect that.
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
}
@Test
public void testPlayWhenReady_changesOnStatusUpdates() {
assertThat(castPlayer.getPlayWhenReady()).isFalse();
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
remoteMediaClientListener.onStatusUpdated();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
assertThat(castPlayer.getPlayWhenReady()).isTrue();
}
@Test
public void testSetRepeatMode_masksRemoteState() {
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
// There is a status update in the middle, which should be hidden by masking.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
remoteMediaClientListener.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the mediaStatus now exposes the new repeat mode.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verifyNoMoreInteractions(mockListener);
}
@Test
public void testSetRepeatMode_updatesUponResultChange() {
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
// There is a status update in the middle, which should be hidden by masking.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
remoteMediaClientListener.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the repeat mode is ALL. The state should reflect that.
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL);
}
@Test
public void testRepeatMode_changesOnStatusUpdates() {
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
remoteMediaClientListener.onStatusUpdated();
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
}
}
/*
* 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.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.ext.cast.MediaItem.DrmConfiguration;
import com.google.android.gms.cast.MediaQueueItem;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Test for {@link DefaultMediaItemConverter}. */
@RunWith(AndroidJUnit4.class)
public class DefaultMediaItemConverterTest {
@Test
public void serialize_deserialize_minimal() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item = builder.setUri(Uri.parse("http://example.com")).setMimeType("mime").build();
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
MediaItem reconstructedItem = converter.toMediaItem(queueItem);
assertThat(reconstructedItem).isEqualTo(item);
}
@Test
public void serialize_deserialize_complete() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item =
builder
.setUri(Uri.parse("http://example.com"))
.setTitle("title")
.setMimeType("mime")
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("http://license.com"),
Collections.singletonMap("key", "value")))
.build();
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
MediaItem reconstructedItem = converter.toMediaItem(queueItem);
assertThat(reconstructedItem).isEqualTo(item);
}
}
...@@ -21,10 +21,7 @@ import android.net.Uri; ...@@ -21,10 +21,7 @@ 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.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -33,112 +30,57 @@ import org.junit.runner.RunWith; ...@@ -33,112 +30,57 @@ import org.junit.runner.RunWith;
public class MediaItemTest { public class MediaItemTest {
@Test @Test
public void buildMediaItem_resetsUuid() {
MediaItem.Builder builder = new MediaItem.Builder();
UUID uuid = new UUID(1, 1);
MediaItem item1 = builder.setUuid(uuid).build();
MediaItem item2 = builder.build();
MediaItem item3 = builder.build();
assertThat(item1.uuid).isEqualTo(uuid);
assertThat(item2.uuid).isNotEqualTo(uuid);
assertThat(item3.uuid).isNotEqualTo(item2.uuid);
assertThat(item3.uuid).isNotEqualTo(uuid);
}
@Test
public void buildMediaItem_doesNotChangeState() { public void buildMediaItem_doesNotChangeState() {
MediaItem.Builder builder = new MediaItem.Builder(); MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item1 = MediaItem item1 =
builder builder
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("http://example.com"))
.setMedia("http://example.com")
.setTitle("title") .setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4) .setMimeType(MimeTypes.AUDIO_MP4)
.setStartPositionUs(3)
.setEndPositionUs(4)
.build(); .build();
MediaItem item2 = builder.setUuid(new UUID(0, 1)).build(); MediaItem item2 = builder.build();
assertThat(item1).isEqualTo(item2); assertThat(item1).isEqualTo(item2);
} }
@Test @Test
public void buildMediaItem_assertDefaultValues() {
assertDefaultValues(new MediaItem.Builder().build());
}
@Test
public void buildAndClear_assertDefaultValues() {
MediaItem.Builder builder = new MediaItem.Builder();
builder
.setMedia("http://example.com")
.setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4)
.setStartPositionUs(3)
.setEndPositionUs(4)
.buildAndClear();
assertDefaultValues(builder.build());
}
@Test
public void equals_withEqualDrmSchemes_returnsTrue() { public void equals_withEqualDrmSchemes_returnsTrue() {
MediaItem.Builder builder = new MediaItem.Builder(); MediaItem.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 = MediaItem mediaItem1 =
builder builder1
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(1))
.setDrmSchemes(createDummyDrmSchemes(1)) .build();
.buildAndClear(); MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 = MediaItem mediaItem2 =
builder builder2
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(1))
.setDrmSchemes(createDummyDrmSchemes(1)) .build();
.buildAndClear();
assertThat(mediaItem1).isEqualTo(mediaItem2); assertThat(mediaItem1).isEqualTo(mediaItem2);
} }
@Test @Test
public void equals_withDifferentDrmRequestHeaders_returnsFalse() { public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
MediaItem.Builder builder = new MediaItem.Builder(); MediaItem.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 = MediaItem mediaItem1 =
builder builder1
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(1))
.setDrmSchemes(createDummyDrmSchemes(1)) .build();
.buildAndClear(); MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 = MediaItem mediaItem2 =
builder builder2
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(2))
.setDrmSchemes(createDummyDrmSchemes(2)) .build();
.buildAndClear();
assertThat(mediaItem1).isNotEqualTo(mediaItem2); assertThat(mediaItem1).isNotEqualTo(mediaItem2);
} }
private static void assertDefaultValues(MediaItem item) { private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) {
assertThat(item.title).isEmpty(); HashMap<String, String> requestHeaders = new HashMap<>();
assertThat(item.description).isEmpty(); requestHeaders.put("key1", "value1");
assertThat(item.media.uri).isEqualTo(Uri.EMPTY); requestHeaders.put("key2", "value2" + seed);
assertThat(item.attachment).isNull(); return new MediaItem.DrmConfiguration(
assertThat(item.drmSchemes).isEmpty(); C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders);
assertThat(item.startPositionUs).isEqualTo(C.TIME_UNSET);
assertThat(item.endPositionUs).isEqualTo(C.TIME_UNSET);
assertThat(item.mimeType).isEmpty();
}
private static List<MediaItem.DrmScheme> createDummyDrmSchemes(int seed) {
HashMap<String, String> requestHeaders1 = new HashMap<>();
requestHeaders1.put("key1", "value1");
requestHeaders1.put("key2", "value1");
MediaItem.UriBundle uriBundle1 =
new MediaItem.UriBundle(Uri.parse("www.uri1.com"), requestHeaders1);
MediaItem.DrmScheme drmScheme1 = new MediaItem.DrmScheme(C.WIDEVINE_UUID, uriBundle1);
HashMap<String, String> requestHeaders2 = new HashMap<>();
requestHeaders2.put("key3", "value3");
requestHeaders2.put("key4", "valueWithSeed" + seed);
MediaItem.UriBundle uriBundle2 =
new MediaItem.UriBundle(Uri.parse("www.uri2.com"), requestHeaders2);
MediaItem.DrmScheme drmScheme2 = new MediaItem.DrmScheme(C.PLAYREADY_UUID, uriBundle2);
return Arrays.asList(drmScheme1, drmScheme2);
} }
} }
...@@ -31,11 +31,13 @@ android { ...@@ -31,11 +31,13 @@ android {
} }
dependencies { dependencies {
api 'org.chromium.net:cronet-embedded:75.3770.101' api 'org.chromium.net:cronet-embedded:76.3809.111'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cronet; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cronet;
import android.content.Context; import android.content.Context;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
...@@ -37,8 +38,8 @@ public final class CronetEngineWrapper { ...@@ -37,8 +38,8 @@ public final class CronetEngineWrapper {
private static final String TAG = "CronetEngineWrapper"; private static final String TAG = "CronetEngineWrapper";
private final CronetEngine cronetEngine; @Nullable private final CronetEngine cronetEngine;
private final @CronetEngineSource int cronetEngineSource; @CronetEngineSource private final int cronetEngineSource;
/** /**
* Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link * Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link
...@@ -144,7 +145,8 @@ public final class CronetEngineWrapper { ...@@ -144,7 +145,8 @@ public final class CronetEngineWrapper {
* *
* @return A {@link CronetEngineSource} value. * @return A {@link CronetEngineSource} value.
*/ */
public @CronetEngineSource int getCronetEngineSource() { @CronetEngineSource
public int getCronetEngineSource() {
return cronetEngineSource; return cronetEngineSource;
} }
...@@ -153,13 +155,14 @@ public final class CronetEngineWrapper { ...@@ -153,13 +155,14 @@ public final class CronetEngineWrapper {
* *
* @return The CronetEngine, or null if no CronetEngine is available. * @return The CronetEngine, or null if no CronetEngine is available.
*/ */
@Nullable
/* package */ CronetEngine getCronetEngine() { /* package */ CronetEngine getCronetEngine() {
return cronetEngine; return cronetEngine;
} }
private static class CronetProviderComparator implements Comparator<CronetProvider> { private static class CronetProviderComparator implements Comparator<CronetProvider> {
private final String gmsCoreCronetName; @Nullable private final String gmsCoreCronetName;
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.
......
/*
* 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.
*/
@NonNullApi
package com.google.android.exoplayer2.ext.cronet;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -14,4 +14,6 @@ ...@@ -14,4 +14,6 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.ext.cronet"/> <manifest package="com.google.android.exoplayer2.ext.cronet">
<uses-sdk/>
</manifest>
...@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately. ...@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE [Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
## Build instructions ## ## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's its modules locally. Instructions for doing this can be found in ExoPlayer's
...@@ -21,15 +21,15 @@ for more information). ...@@ -21,15 +21,15 @@ for more information).
In addition, it's necessary to build the extension's native components as In addition, it's necessary to build the extension's native components as
follows: follows:
* Set the following environment variables: * Set the following shell variable:
``` ```
cd "<path to exoplayer checkout>" cd "<path to exoplayer checkout>"
FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni" FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni"
``` ```
* Download the [Android NDK][] and set its location in an environment variable. * Download the [Android NDK][] and set its location in a shell variable.
Only versions up to NDK 15c are supported currently. This build configuration has been tested on NDK r20.
``` ```
NDK_PATH="<path to Android NDK>" NDK_PATH="<path to Android NDK>"
...@@ -41,70 +41,21 @@ NDK_PATH="<path to Android NDK>" ...@@ -41,70 +41,21 @@ NDK_PATH="<path to Android NDK>"
HOST_PLATFORM="linux-x86_64" HOST_PLATFORM="linux-x86_64"
``` ```
* Fetch and build FFmpeg. The configuration flags determine which formats will * Configure the formats supported by adapting the following variable if needed
be supported. See the [Supported formats][] page for more details of the and by setting it. See the [Supported formats][] page for more details of the
available flags. formats.
For example, to fetch and build FFmpeg release 4.0 for armeabi-v7a, ```
arm64-v8a and x86 on Linux x86_64: ENABLED_DECODERS=(vorbis opus flac)
```
* Fetch and build FFmpeg. For example, executing script `build_ffmpeg.sh` will
fetch and build FFmpeg release 4.2 for armeabi-v7a, arm64-v8a and x86:
``` ```
COMMON_OPTIONS="\
--target-os=android \
--disable-static \
--enable-shared \
--disable-doc \
--disable-programs \
--disable-everything \
--disable-avdevice \
--disable-avformat \
--disable-swscale \
--disable-postproc \
--disable-avfilter \
--disable-symver \
--disable-swresample \
--enable-avresample \
--enable-decoder=vorbis \
--enable-decoder=opus \
--enable-decoder=flac \
" && \
cd "${FFMPEG_EXT_PATH}" && \ cd "${FFMPEG_EXT_PATH}" && \
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \ ./build_ffmpeg.sh \
cd ffmpeg && git checkout release/4.0 && \ "${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
./configure \
--libdir=android-libs/armeabi-v7a \
--arch=arm \
--cpu=armv7-a \
--cross-prefix="${NDK_PATH}/toolchains/arm-linux-androideabi-4.9/prebuilt/${HOST_PLATFORM}/bin/arm-linux-androideabi-" \
--sysroot="${NDK_PATH}/platforms/android-9/arch-arm/" \
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
--extra-ldflags="-Wl,--fix-cortex-a8" \
--extra-ldexeflags=-pie \
${COMMON_OPTIONS} \
&& \
make -j4 && make install-libs && \
make clean && ./configure \
--libdir=android-libs/arm64-v8a \
--arch=aarch64 \
--cpu=armv8-a \
--cross-prefix="${NDK_PATH}/toolchains/aarch64-linux-android-4.9/prebuilt/${HOST_PLATFORM}/bin/aarch64-linux-android-" \
--sysroot="${NDK_PATH}/platforms/android-21/arch-arm64/" \
--extra-ldexeflags=-pie \
${COMMON_OPTIONS} \
&& \
make -j4 && make install-libs && \
make clean && ./configure \
--libdir=android-libs/x86 \
--arch=x86 \
--cpu=i686 \
--cross-prefix="${NDK_PATH}/toolchains/x86-4.9/prebuilt/${HOST_PLATFORM}/bin/i686-linux-android-" \
--sysroot="${NDK_PATH}/platforms/android-9/arch-x86/" \
--extra-ldexeflags=-pie \
--disable-asm \
${COMMON_OPTIONS} \
&& \
make -j4 && make install-libs && \
make clean
``` ```
* Build the JNI native libraries, setting `APP_ABI` to include the architectures * Build the JNI native libraries, setting `APP_ABI` to include the architectures
...@@ -115,28 +66,35 @@ cd "${FFMPEG_EXT_PATH}" && \ ...@@ -115,28 +66,35 @@ 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" -j4
``` ```
## Build instructions (Windows) ##
We do not provide support for building this extension on Windows, however it
should be possible to follow the Linux instructions in [Windows PowerShell][].
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
## Using the extension ## ## Using the extension ##
Once you've followed the instructions above to check out, build and depend on Once you've followed the instructions above to check out, build and depend on
the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`. the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`.
How you do this depends on which player API you're using: How you do this depends on which player API you're using:
* If you're passing a `DefaultRenderersFactory` to * If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
`ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by you can enable using the extension by setting the `extensionRendererMode`
setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` parameter of the `DefaultRenderersFactory` constructor to
constructor to `EXTENSION_RENDERER_MODE_ON`. This will use `EXTENSION_RENDERER_MODE_ON`. This will use `FfmpegAudioRenderer` for playback
`FfmpegAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't if `MediaCodecAudioRenderer` doesn't support the input format. Pass
support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give `EXTENSION_RENDERER_MODE_PREFER` to give `FfmpegAudioRenderer` priority over
`FfmpegAudioRenderer` priority over `MediaCodecAudioRenderer`. `MediaCodecAudioRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer` * If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer`
to the output list in `buildAudioRenderers`. ExoPlayer will use the first to the output list in `buildAudioRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format. `Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return an * If you've implemented your own `RenderersFactory`, return an
`FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the `FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format. first `Renderer` in the returned array that supports the input media format.
* If you're using `ExoPlayerFactory.newInstance`, pass an `FfmpegAudioRenderer` * If you're using `ExoPlayer.Builder`, pass an `FfmpegAudioRenderer` in the
in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
list that supports the input media format. supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation, a custom track selector the choice of `Renderer` is up to your implementation,
......
...@@ -38,9 +38,10 @@ android { ...@@ -38,9 +38,10 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -92,8 +92,9 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -92,8 +92,9 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager, @FormatSupport
Format format) { protected int supportsFormatInternal(
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
Assertions.checkNotNull(format.sampleMimeType); Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) { if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
...@@ -108,12 +109,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -108,12 +109,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
@AdaptiveSupport
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
return ADAPTIVE_NOT_SEAMLESS; return ADAPTIVE_NOT_SEAMLESS;
} }
@Override @Override
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException { throws FfmpegDecoderException {
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;
......
...@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; ...@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
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 com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
...@@ -42,7 +43,7 @@ import java.util.List; ...@@ -42,7 +43,7 @@ import java.util.List;
private static final int DECODER_ERROR_OTHER = -2; private static final int DECODER_ERROR_OTHER = -2;
private final String codecName; private final String codecName;
private final @Nullable byte[] extraData; @Nullable private final byte[] extraData;
private final @C.Encoding int encoding; private final @C.Encoding int encoding;
private final int outputBufferSize; private final int outputBufferSize;
...@@ -106,7 +107,7 @@ import java.util.List; ...@@ -106,7 +107,7 @@ import java.util.List;
return new FfmpegDecoderException("Error resetting (see logcat)."); return new FfmpegDecoderException("Error resetting (see logcat).");
} }
} }
ByteBuffer inputData = inputBuffer.data; ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
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);
...@@ -132,8 +133,8 @@ import java.util.List; ...@@ -132,8 +133,8 @@ import java.util.List;
} }
hasOutputFormat = true; hasOutputFormat = true;
} }
outputBuffer.data.position(0); outputData.position(0);
outputBuffer.data.limit(result); outputData.limit(result);
return null; return null;
} }
......
...@@ -34,7 +34,7 @@ public final class FfmpegLibrary { ...@@ -34,7 +34,7 @@ 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", "avcodec", "ffmpeg"); new LibraryLoader("avutil", "avresample", "swresample", "avcodec", "ffmpeg");
private FfmpegLibrary() {} private FfmpegLibrary() {}
......
/*
* 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.
*/
@NonNullApi
package com.google.android.exoplayer2.ext.ffmpeg;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -22,12 +22,17 @@ LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so ...@@ -22,12 +22,17 @@ LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY) include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS) include $(CLEAR_VARS)
LOCAL_MODULE := libavutil LOCAL_MODULE := libavresample
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) include $(CLEAR_VARS)
LOCAL_MODULE := libavresample LOCAL_MODULE := libswresample
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libavutil
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY) include $(PREBUILT_SHARED_LIBRARY)
...@@ -35,6 +40,6 @@ include $(CLEAR_VARS) ...@@ -35,6 +40,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 libavutil LOCAL_SHARED_LIBRARIES := libavcodec libavresample 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)
...@@ -15,6 +15,6 @@ ...@@ -15,6 +15,6 @@
# #
APP_OPTIM := release APP_OPTIM := release
APP_STL := gnustl_static APP_STL := c++_static
APP_CPPFLAGS := -frtti APP_CPPFLAGS := -frtti
APP_PLATFORM := android-9 APP_PLATFORM := android-9
#!/bin/bash
#
# 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.
#
FFMPEG_EXT_PATH=$1
NDK_PATH=$2
HOST_PLATFORM=$3
ENABLED_DECODERS=("${@:4}")
COMMON_OPTIONS="
--target-os=android
--disable-static
--enable-shared
--disable-doc
--disable-programs
--disable-everything
--disable-avdevice
--disable-avformat
--disable-swscale
--disable-postproc
--disable-avfilter
--disable-symver
--enable-avresample
--enable-swresample
"
TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin"
for decoder in "${ENABLED_DECODERS[@]}"
do
COMMON_OPTIONS="${COMMON_OPTIONS} --enable-decoder=${decoder}"
done
cd "${FFMPEG_EXT_PATH}"
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg)
cd ffmpeg
git checkout release/4.2
./configure \
--libdir=android-libs/armeabi-v7a \
--arch=arm \
--cpu=armv7-a \
--cross-prefix="${TOOLCHAIN_PREFIX}/armv7a-linux-androideabi16-" \
--nm="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-nm" \
--strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
--extra-ldflags="-Wl,--fix-cortex-a8" \
--extra-ldexeflags=-pie \
${COMMON_OPTIONS}
make -j4
make install-libs
make clean
./configure \
--libdir=android-libs/arm64-v8a \
--arch=aarch64 \
--cpu=armv8-a \
--cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \
--nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \
--strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \
--extra-ldexeflags=-pie \
${COMMON_OPTIONS}
make -j4
make install-libs
make clean
./configure \
--libdir=android-libs/x86 \
--arch=x86 \
--cpu=i686 \
--cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \
--nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \
--strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \
--extra-ldexeflags=-pie \
--disable-asm \
${COMMON_OPTIONS}
make -j4
make install-libs
make clean
...@@ -14,4 +14,6 @@ ...@@ -14,4 +14,6 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.ext.ffmpeg"/> <manifest package="com.google.android.exoplayer2.ext.ffmpeg">
<uses-sdk/>
</manifest>
...@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately. ...@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE [Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
## Build instructions ## ## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's its modules locally. Instructions for doing this can be found in ExoPlayer's
...@@ -28,8 +28,8 @@ EXOPLAYER_ROOT="$(pwd)" ...@@ -28,8 +28,8 @@ EXOPLAYER_ROOT="$(pwd)"
FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main" FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
``` ```
* Download the [Android NDK][] (version <= 17c) and set its location in an * Download the [Android NDK][] and set its location in an environment variable.
environment variable: This build configuration has been tested on NDK r20.
``` ```
NDK_PATH="<path to Android NDK>" NDK_PATH="<path to Android NDK>"
...@@ -53,6 +53,13 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 ...@@ -53,6 +53,13 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
## Build instructions (Windows) ##
We do not provide support for building this extension on Windows, however it
should be possible to follow the Linux instructions in [Windows PowerShell][].
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
## Using the extension ## ## Using the extension ##
Once you've followed the instructions above to check out, build and depend on Once you've followed the instructions above to check out, build and depend on
...@@ -68,22 +75,22 @@ renderer. ...@@ -68,22 +75,22 @@ renderer.
### Using `LibflacAudioRenderer` ### ### Using `LibflacAudioRenderer` ###
* If you're passing a `DefaultRenderersFactory` to * If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
`ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by you can enable using the extension by setting the `extensionRendererMode`
setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` parameter of the `DefaultRenderersFactory` constructor to
constructor to `EXTENSION_RENDERER_MODE_ON`. This will use `EXTENSION_RENDERER_MODE_ON`. This will use `LibflacAudioRenderer` for
`LibflacAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't playback if `MediaCodecAudioRenderer` doesn't support the input format. Pass
support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give `EXTENSION_RENDERER_MODE_PREFER` to give `LibflacAudioRenderer` priority over
`LibflacAudioRenderer` priority over `MediaCodecAudioRenderer`. `MediaCodecAudioRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer` * If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer`
to the output list in `buildAudioRenderers`. ExoPlayer will use the first to the output list in `buildAudioRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format. `Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return a * If you've implemented your own `RenderersFactory`, return a
`LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use the `LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format. first `Renderer` in the returned array that supports the input media format.
* If you're using `ExoPlayerFactory.newInstance`, pass a `LibflacAudioRenderer` * If you're using `ExoPlayer.Builder`, pass a `LibflacAudioRenderer` in the
in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
list that supports the input media format. supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation, a custom track selector the choice of `Renderer` is up to your implementation,
......
...@@ -39,11 +39,14 @@ android { ...@@ -39,11 +39,14 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation 'androidx.test:core:' + androidxTestCoreVersion
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.testutil.FakeExtractorInput; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException; import java.io.IOException;
import org.junit.Before; import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Unit test for {@link FlacBinarySearchSeeker}. */ /** Unit test for {@link FlacBinarySearchSeeker}. */
...@@ -41,6 +42,7 @@ public final class FlacBinarySearchSeekerTest { ...@@ -41,6 +42,7 @@ public final class FlacBinarySearchSeekerTest {
} }
} }
@Test
public void testGetSeekMap_returnsSeekMapWithCorrectDuration() public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
throws IOException, FlacDecoderException, InterruptedException { throws IOException, FlacDecoderException, InterruptedException {
byte[] data = byte[] data =
...@@ -63,6 +65,7 @@ public final class FlacBinarySearchSeekerTest { ...@@ -63,6 +65,7 @@ public final class FlacBinarySearchSeekerTest {
assertThat(seekMap.isSeekable()).isTrue(); assertThat(seekMap.isSeekable()).isTrue();
} }
@Test
public void testSetSeekTargetUs_returnsSeekPending() public void testSetSeekTargetUs_returnsSeekPending()
throws IOException, FlacDecoderException, InterruptedException { throws IOException, FlacDecoderException, InterruptedException {
byte[] data = byte[] data =
......
...@@ -41,6 +41,7 @@ import java.io.IOException; ...@@ -41,6 +41,7 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import org.junit.Before; import org.junit.Before;
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} when the FLAC stream does not have a SEEKTABLE. */
...@@ -76,6 +77,7 @@ public final class FlacExtractorSeekTest { ...@@ -76,6 +77,7 @@ public final class FlacExtractorSeekTest {
positionHolder = new PositionHolder(); positionHolder = new PositionHolder();
} }
@Test
public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap() public void testFlacExtractorReads_nonSeekTableFile_returnSeekableSeekMap()
throws IOException, InterruptedException { throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor(); FlacExtractor extractor = new FlacExtractor();
...@@ -87,6 +89,7 @@ public final class FlacExtractorSeekTest { ...@@ -87,6 +89,7 @@ public final class FlacExtractorSeekTest {
assertThat(seekMap.isSeekable()).isTrue(); assertThat(seekMap.isSeekable()).isTrue();
} }
@Test
public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame() public void testHandlePendingSeek_handlesSeekingToPositionInFile_extractsCorrectFrame()
throws IOException, InterruptedException { throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor(); FlacExtractor extractor = new FlacExtractor();
...@@ -103,6 +106,7 @@ public final class FlacExtractorSeekTest { ...@@ -103,6 +106,7 @@ public final class FlacExtractorSeekTest {
trackOutput, targetSeekTimeUs, extractedFrameIndex); trackOutput, targetSeekTimeUs, extractedFrameIndex);
} }
@Test
public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame() public void testHandlePendingSeek_handlesSeekToEoF_extractsLastFrame()
throws IOException, InterruptedException { throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor(); FlacExtractor extractor = new FlacExtractor();
...@@ -120,6 +124,7 @@ public final class FlacExtractorSeekTest { ...@@ -120,6 +124,7 @@ public final class FlacExtractorSeekTest {
trackOutput, targetSeekTimeUs, extractedFrameIndex); trackOutput, targetSeekTimeUs, extractedFrameIndex);
} }
@Test
public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame() public void testHandlePendingSeek_handlesSeekingBackward_extractsCorrectFrame()
throws IOException, InterruptedException { throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor(); FlacExtractor extractor = new FlacExtractor();
...@@ -139,6 +144,7 @@ public final class FlacExtractorSeekTest { ...@@ -139,6 +144,7 @@ public final class FlacExtractorSeekTest {
trackOutput, targetSeekTimeUs, extractedFrameIndex); trackOutput, targetSeekTimeUs, extractedFrameIndex);
} }
@Test
public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame() public void testHandlePendingSeek_handlesSeekingForward_extractsCorrectFrame()
throws IOException, InterruptedException { throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor(); FlacExtractor extractor = new FlacExtractor();
...@@ -158,6 +164,7 @@ public final class FlacExtractorSeekTest { ...@@ -158,6 +164,7 @@ public final class FlacExtractorSeekTest {
trackOutput, targetSeekTimeUs, extractedFrameIndex); trackOutput, targetSeekTimeUs, extractedFrameIndex);
} }
@Test
public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame() public void testHandlePendingSeek_handlesRandomSeeks_extractsCorrectFrame()
throws IOException, InterruptedException { throws IOException, InterruptedException {
FlacExtractor extractor = new FlacExtractor(); FlacExtractor extractor = new FlacExtractor();
...@@ -228,7 +235,8 @@ public final class FlacExtractorSeekTest { ...@@ -228,7 +235,8 @@ public final class FlacExtractorSeekTest {
} }
} }
private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output) @Nullable
private SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
throws IOException, InterruptedException { throws IOException, InterruptedException {
try { try {
ExtractorInput input = getExtractorInputFromPosition(0); ExtractorInput input = getExtractorInputFromPosition(0);
......
...@@ -21,6 +21,7 @@ import androidx.test.core.app.ApplicationProvider; ...@@ -21,6 +21,7 @@ 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;
import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Unit test for {@link FlacExtractor}. */ /** Unit test for {@link FlacExtractor}. */
...@@ -34,11 +35,13 @@ public class FlacExtractorTest { ...@@ -34,11 +35,13 @@ public class FlacExtractorTest {
} }
} }
@Test
public void testExtractFlacSample() throws Exception { public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior( ExtractorAsserts.assertBehavior(
FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext()); FlacExtractor::new, "bear.flac", ApplicationProvider.getApplicationContext());
} }
@Test
public void testExtractFlacSampleWithId3Header() throws Exception { public void testExtractFlacSampleWithId3Header() throws Exception {
ExtractorAsserts.assertBehavior( ExtractorAsserts.assertBehavior(
FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext()); FlacExtractor::new, "bear_with_id3.flac", ApplicationProvider.getApplicationContext());
......
...@@ -24,13 +24,10 @@ import androidx.test.core.app.ApplicationProvider; ...@@ -24,13 +24,10 @@ 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.ExoPlayerFactory;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.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.trackselection.DefaultTrackSelector;
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;
...@@ -82,8 +79,7 @@ public class FlacPlaybackTest { ...@@ -82,8 +79,7 @@ public class FlacPlaybackTest {
public void run() { public void run() {
Looper.prepare(); Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer(); LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = new ExoPlayer.Builder(context, audioRenderer).build();
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
MediaSource mediaSource = MediaSource mediaSource =
new ProgressiveMediaSource.Factory( new ProgressiveMediaSource.Factory(
...@@ -101,7 +97,7 @@ public class FlacPlaybackTest { ...@@ -101,7 +97,7 @@ public class FlacPlaybackTest {
} }
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, @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();
......
...@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; ...@@ -22,6 +22,7 @@ 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.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
...@@ -101,7 +102,7 @@ import java.util.List; ...@@ -101,7 +102,7 @@ import java.util.List;
if (reset) { if (reset) {
decoderJni.flush(); decoderJni.flush();
} }
decoderJni.setData(inputBuffer.data); decoderJni.setData(Util.castNonNull(inputBuffer.data));
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
try { try {
decoderJni.decodeSample(outputData); decoderJni.decodeSample(outputData);
......
...@@ -51,6 +51,12 @@ import java.nio.ByteBuffer; ...@@ -51,6 +51,12 @@ 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.");
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import android.os.Handler; import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; 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;
...@@ -33,7 +34,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -33,7 +34,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
private static final int NUM_BUFFERS = 16; private static final int NUM_BUFFERS = 16;
public LibflacAudioRenderer() { public LibflacAudioRenderer() {
this(null, null); this(/* eventHandler= */ null, /* eventListener= */ null);
} }
/** /**
...@@ -43,15 +44,16 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -43,15 +44,16 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/ */
public LibflacAudioRenderer( public LibflacAudioRenderer(
Handler eventHandler, @Nullable Handler eventHandler,
AudioRendererEventListener eventListener, @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) { AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors); super(eventHandler, eventListener, audioProcessors);
} }
@Override @Override
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager, @FormatSupport
Format format) { protected int supportsFormatInternal(
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, 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;
...@@ -65,7 +67,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -65,7 +67,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FlacDecoderException { throws FlacDecoderException {
return new FlacDecoder( return new FlacDecoder(
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
......
/*
* 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.
*/
@NonNullApi
package com.google.android.exoplayer2.ext.flac;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -15,6 +15,6 @@ ...@@ -15,6 +15,6 @@
# #
APP_OPTIM := release APP_OPTIM := release
APP_STL := gnustl_static APP_STL := c++_static
APP_CPPFLAGS := -frtti APP_CPPFLAGS := -frtti
APP_PLATFORM := android-14 APP_PLATFORM := android-14
...@@ -265,11 +265,11 @@ FLACParser::FLACParser(DataSource *source) ...@@ -265,11 +265,11 @@ FLACParser::FLACParser(DataSource *source)
: mDataSource(source), : mDataSource(source),
mCopy(copyTrespass), mCopy(copyTrespass),
mDecoder(NULL), mDecoder(NULL),
mSeekTable(NULL),
firstFrameOffset(0LL),
mCurrentPos(0LL), mCurrentPos(0LL),
mEOF(false), mEOF(false),
mStreamInfoValid(false), mStreamInfoValid(false),
mSeekTable(NULL),
firstFrameOffset(0LL),
mVorbisCommentsValid(false), mVorbisCommentsValid(false),
mPicturesValid(false), mPicturesValid(false),
mWriteRequested(false), mWriteRequested(false),
...@@ -456,6 +456,9 @@ bool FLACParser::getSeekPositions(int64_t timeUs, ...@@ -456,6 +456,9 @@ bool FLACParser::getSeekPositions(int64_t timeUs,
for (unsigned i = length; i != 0; i--) { for (unsigned i = length; i != 0; i--) {
int64_t sampleNumber = points[i - 1].sample_number; int64_t sampleNumber = points[i - 1].sample_number;
if (sampleNumber == -1) { // placeholder
continue;
}
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;
......
...@@ -62,7 +62,9 @@ class FLACParser { ...@@ -62,7 +62,9 @@ class FLACParser {
bool areVorbisCommentsValid() const { return mVorbisCommentsValid; } bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
std::vector<std::string> getVorbisComments() { return mVorbisComments; } const std::vector<std::string>& getVorbisComments() const {
return mVorbisComments;
}
bool arePicturesValid() const { return mPicturesValid; } bool arePicturesValid() const { return mPicturesValid; }
......
...@@ -14,4 +14,6 @@ ...@@ -14,4 +14,6 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.ext.flac"/> <manifest package="com.google.android.exoplayer2.ext.flac">
<uses-sdk/>
</manifest>
# ExoPlayer GVR extension # # ExoPlayer GVR extension #
**DEPRECATED - If you still need this extension, please contact us by filing an
issue on our [issue tracker][].**
The GVR extension wraps the [Google VR SDK for Android][]. It provides a The GVR extension wraps the [Google VR SDK for Android][]. It provides a
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
of surround sound and ambisonic soundfields. of surround sound and ambisonic soundfields.
[Google VR SDK for Android]: https://developers.google.com/vr/android/ [Google VR SDK for Android]: https://developers.google.com/vr/android/
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround [GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
[issue tracker]: https://github.com/google/ExoPlayer/issues
## Getting the extension ## ## Getting the extension ##
......
...@@ -33,7 +33,7 @@ android { ...@@ -33,7 +33,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation 'androidx.annotation:annotation:1.1.0' 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
} }
......
...@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.gvr; ...@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.gvr;
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.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
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.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.vr.sdk.audio.GvrAudioSurround; import com.google.vr.sdk.audio.GvrAudioSurround;
...@@ -28,7 +27,11 @@ import java.nio.ByteOrder; ...@@ -28,7 +27,11 @@ import java.nio.ByteOrder;
/** /**
* An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of * An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of
* surround sound and ambisonic soundfields. * surround sound and ambisonic soundfields.
*
* @deprecated If you still need this component, please contact us by filing an issue on our <a
* href="https://github.com/google/ExoPlayer/issues">issue tracker</a>.
*/ */
@Deprecated
public final class GvrAudioProcessor implements AudioProcessor { public final class GvrAudioProcessor implements AudioProcessor {
static { static {
...@@ -40,8 +43,7 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -40,8 +43,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output. private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID; private static final int NO_SURROUND_FORMAT = GvrAudioSurround.SurroundFormat.INVALID;
private int sampleRateHz; private AudioFormat pendingInputAudioFormat;
private int channelCount;
private int pendingGvrAudioSurroundFormat; private int pendingGvrAudioSurroundFormat;
@Nullable private GvrAudioSurround gvrAudioSurround; @Nullable private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer; private ByteBuffer buffer;
...@@ -56,8 +58,7 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -56,8 +58,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
public GvrAudioProcessor() { public GvrAudioProcessor() {
// Use the identity for the initial orientation. // Use the identity for the initial orientation.
w = 1f; w = 1f;
sampleRateHz = Format.NO_VALUE; pendingInputAudioFormat = AudioFormat.NOT_SET;
channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER; buffer = EMPTY_BUFFER;
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
} }
...@@ -83,19 +84,13 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -83,19 +84,13 @@ public final class GvrAudioProcessor implements AudioProcessor {
@SuppressWarnings("ReferenceEquality") @SuppressWarnings("ReferenceEquality")
@Override @Override
public synchronized boolean configure( public synchronized AudioFormat configure(AudioFormat inputAudioFormat)
int sampleRateHz, int channelCount, @C.Encoding int encoding) throws UnhandledAudioFormatException {
throws UnhandledFormatException { if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
if (encoding != C.ENCODING_PCM_16BIT) {
maybeReleaseGvrAudioSurround(); maybeReleaseGvrAudioSurround();
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); throw new UnhandledAudioFormatException(inputAudioFormat);
}
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
return false;
} }
this.sampleRateHz = sampleRateHz; switch (inputAudioFormat.channelCount) {
this.channelCount = channelCount;
switch (channelCount) {
case 1: case 1:
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO; pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
break; break;
...@@ -115,13 +110,14 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -115,13 +110,14 @@ public final class GvrAudioProcessor implements AudioProcessor {
pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS; pendingGvrAudioSurroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
break; break;
default: default:
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); throw new UnhandledAudioFormatException(inputAudioFormat);
} }
if (buffer == EMPTY_BUFFER) { if (buffer == EMPTY_BUFFER) {
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE) buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
.order(ByteOrder.nativeOrder()); .order(ByteOrder.nativeOrder());
} }
return true; pendingInputAudioFormat = inputAudioFormat;
return new AudioFormat(inputAudioFormat.sampleRate, OUTPUT_CHANNEL_COUNT, C.ENCODING_PCM_16BIT);
} }
@Override @Override
...@@ -130,21 +126,6 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -130,21 +126,6 @@ public final class GvrAudioProcessor implements AudioProcessor {
} }
@Override @Override
public int getOutputChannelCount() {
return OUTPUT_CHANNEL_COUNT;
}
@Override
public int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public int getOutputSampleRateHz() {
return sampleRateHz;
}
@Override
public void queueInput(ByteBuffer input) { public void queueInput(ByteBuffer input) {
int position = input.position(); int position = input.position();
Assertions.checkNotNull(gvrAudioSurround); Assertions.checkNotNull(gvrAudioSurround);
...@@ -182,7 +163,10 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -182,7 +163,10 @@ public final class GvrAudioProcessor implements AudioProcessor {
maybeReleaseGvrAudioSurround(); maybeReleaseGvrAudioSurround();
gvrAudioSurround = gvrAudioSurround =
new GvrAudioSurround( new GvrAudioSurround(
pendingGvrAudioSurroundFormat, sampleRateHz, channelCount, FRAMES_PER_OUTPUT_BUFFER); pendingGvrAudioSurroundFormat,
pendingInputAudioFormat.sampleRate,
pendingInputAudioFormat.channelCount,
FRAMES_PER_OUTPUT_BUFFER);
gvrAudioSurround.updateNativeOrientation(w, x, y, z); gvrAudioSurround.updateNativeOrientation(w, x, y, z);
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
} else if (gvrAudioSurround != null) { } else if (gvrAudioSurround != null) {
...@@ -196,8 +180,7 @@ public final class GvrAudioProcessor implements AudioProcessor { ...@@ -196,8 +180,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
maybeReleaseGvrAudioSurround(); maybeReleaseGvrAudioSurround();
updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f); updateOrientation(/* w= */ 1f, /* x= */ 0f, /* y= */ 0f, /* z= */ 0f);
inputEnded = false; inputEnded = false;
sampleRateHz = Format.NO_VALUE; pendingInputAudioFormat = AudioFormat.NOT_SET;
channelCount = Format.NO_VALUE;
buffer = EMPTY_BUFFER; buffer = EMPTY_BUFFER;
pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT; pendingGvrAudioSurroundFormat = NO_SURROUND_FORMAT;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment