Commit afba6386 by ojw28 Committed by GitHub

Merge pull request #4881 from google/dev-v2-r2.9.0

r2.9.0
parents 09d1e3e3 1f9a003b
Showing with 1682 additions and 521 deletions
......@@ -17,8 +17,9 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.0'
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.novoda:bintray-release:0.8.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3'
}
// Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070
......
......@@ -13,19 +13,18 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.8.4'
releaseVersionCode = 2804
releaseVersion = '2.9.0'
releaseVersionCode = 2009000
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided
// by the library requires API level 16 or greater.
minSdkVersion = 14
targetSdkVersion = 27
compileSdkVersion = 27
buildToolsVersion = '27.0.3'
targetSdkVersion = 28
compileSdkVersion = 28
buildToolsVersion = '28.0.2'
testSupportLibraryVersion = '0.5'
supportLibraryVersion = '27.0.0'
playServicesLibraryVersion = '15.0.1'
supportLibraryVersion = '27.1.1'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
junitVersion = '4.12'
......@@ -33,7 +32,7 @@ project.ext {
robolectricVersion = '3.7.1'
autoValueVersion = '1.6'
checkerframeworkVersion = '2.5.0'
testRunnerVersion = '1.0.2'
testRunnerVersion = '1.1.0-alpha3'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
......
......@@ -30,6 +30,7 @@ include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr'
include modulePrefix + 'extension-ima'
include modulePrefix + 'extension-cast'
include modulePrefix + 'extension-cronet'
include modulePrefix + 'extension-mediasession'
include modulePrefix + 'extension-okhttp'
include modulePrefix + 'extension-opus'
......@@ -51,6 +52,7 @@ project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensi
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
......@@ -58,9 +60,3 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
if (gradle.ext.has('exoplayerIncludeCronetExtension')
&& gradle.ext.exoplayerIncludeCronetExtension) {
include modulePrefix + 'extension-cronet'
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
}
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
......@@ -57,3 +62,5 @@ dependencies {
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
......@@ -30,8 +30,6 @@ import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
......@@ -145,13 +143,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
ListView sampleList = dialogList.findViewById(R.id.sample_list);
sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener(
new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
playerManager.addItem(DemoUtil.SAMPLES.get(position));
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
}
(parent, view, position, id) -> {
playerManager.addItem(DemoUtil.SAMPLES.get(position));
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
});
return dialogList;
}
......
......@@ -24,8 +24,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DefaultEventListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
......@@ -37,14 +37,11 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
......@@ -52,11 +49,9 @@ import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.framework.CastContext;
import java.util.ArrayList;
/**
* Manages players and an internal media queue for the ExoPlayer/Cast demo app.
*/
/* package */ final class PlayerManager extends DefaultEventListener
implements CastPlayer.SessionAvailabilityListener {
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
/* package */ final class PlayerManager
implements EventListener, CastPlayer.SessionAvailabilityListener {
/**
* Listener for changes in the media queue playback position.
......@@ -71,9 +66,8 @@ import java.util.ArrayList;
}
private static final String USER_AGENT = "ExoCastDemoPlayer";
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER);
new DefaultHttpDataSourceFactory(USER_AGENT);
private final PlayerView localPlayerView;
private final PlayerControlView castControlView;
......@@ -120,9 +114,9 @@ import java.util.ArrayList;
currentItemIndex = C.INDEX_UNSET;
concatenatingMediaSource = new ConcatenatingMediaSource();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer);
......@@ -397,13 +391,9 @@ import java.util.ArrayList;
Uri uri = Uri.parse(sample.uri);
switch (sample.mimeType) {
case DemoUtil.MIME_TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
.createMediaSource(uri);
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_DASH:
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY)
.createMediaSource(uri);
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_HLS:
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_VIDEO_MP4:
......
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
......@@ -51,3 +56,5 @@ dependencies {
implementation project(modulePrefix + 'extension-ima')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
......@@ -27,18 +27,14 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
......@@ -46,8 +42,7 @@ import com.google.android.exoplayer2.util.Util;
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
private final ImaAdsLoader adsLoader;
private final DataSource.Factory manifestDataSourceFactory;
private final DataSource.Factory mediaDataSourceFactory;
private final DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
private long contentPosition;
......@@ -55,21 +50,14 @@ import com.google.android.exoplayer2.util.Util;
public PlayerManager(Context context) {
String adTag = context.getString(R.string.ad_tag_url);
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
manifestDataSourceFactory =
dataSourceFactory =
new DefaultDataSourceFactory(
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
mediaDataSourceFactory =
new DefaultDataSourceFactory(
context,
Util.getUserAgent(context, context.getString(R.string.application_name)),
new DefaultBandwidthMeter());
}
public void init(Context context, PlayerView playerView) {
// Create a default track selector.
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(bandwidthMeter);
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
// Create a player instance.
......@@ -88,9 +76,7 @@ import com.google.android.exoplayer2.util.Util;
contentMediaSource,
/* adMediaSourceFactory= */ this,
adsLoader,
playerView.getOverlayFrameLayout(),
/* eventHandler= */ null,
/* eventListener= */ null);
playerView.getOverlayFrameLayout());
// Prepare the player with the source.
player.seekTo(contentPosition);
......@@ -133,18 +119,13 @@ import com.google.android.exoplayer2.util.Util;
@ContentType int type = Util.inferContentType(uri);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
manifestDataSourceFactory)
.createMediaSource(uri);
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
.createMediaSource(uri);
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri);
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
......
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
......@@ -70,3 +75,5 @@ dependencies {
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
......@@ -21,6 +21,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
......@@ -79,7 +80,7 @@
<service android:name="com.google.android.exoplayer2.demo.DemoDownloadService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.exoplayer.downloadService.action.INIT"/>
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
......
......@@ -4,22 +4,22 @@
"samples": [
{
"name": "Google Glass (MP4,H264)",
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
"extension": "mpd"
},
{
"name": "Google Play (MP4,H264)",
"uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
"extension": "mpd"
},
{
"name": "Google Glass (WebM,VP9)",
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
"extension": "mpd"
},
{
"name": "Google Play (WebM,VP9)",
"uri": "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
"extension": "mpd"
}
]
......@@ -330,11 +330,11 @@
"samples": [
{
"name": "Super speed",
"uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
},
{
"name": "Super speed (PlayReady)",
"uri": "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
"drm_scheme": "playready"
}
]
......@@ -352,11 +352,11 @@
},
{
"name": "Apple master playlist advanced (TS)",
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8"
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
},
{
"name": "Apple master playlist advanced (fMP4)",
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8"
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
},
{
"name": "Apple TS media playlist",
......@@ -365,10 +365,6 @@
{
"name": "Apple AAC media playlist",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
},
{
"name": "Apple ID3 metadata",
"uri": "http://devimages.apple.com/samplecode/adDemo/ad.m3u8"
}
]
},
......@@ -376,7 +372,7 @@
"name": "Misc",
"samples": [
{
"name": "Dizzy",
"name": "Dizzy (MP4)",
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
......@@ -392,10 +388,6 @@
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
},
{
"name": "Big Buck Bunny (MP4 Video)",
"uri": "http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube&sparams=ip,ipbits,expire,source,id&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=513F28C7FDCBEC60A66C86C9A393556C99DC47FB.04C88036EEE12565A1ED864A875A58F15D8B5300&key=ik0"
},
{
"name": "Screens 360P (WebM,VP9,No Audio)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
},
......@@ -420,20 +412,8 @@
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
},
{
"name": "Google Glass (WebM Video with Vorbis Audio)",
"uri": "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm"
},
{
"name": "Google Glass (VP9 in MP4/ISO-BMFF)",
"uri": "http://demos.webmproject.org/exoplayer/glass.mp4"
},
{
"name": "Google Glass DASH - VP9 and Opus",
"uri": "http://demos.webmproject.org/dash/201410/vp9_glass/manifest_vp9_opus.mpd"
},
{
"name": "Big Buck Bunny (FLV Video)",
"uri": "http://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
}
]
},
......@@ -570,23 +550,32 @@
{
"name": "VMAP empty midroll",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll"
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll"
},
{
"name": "VMAP full, empty, full midrolls",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2"
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2"
}
]
},
{
"name": "ABR",
"name": "360",
"samples": [
{
"name": "Random ABR - Google Glass (MP4,H264)",
"uri": "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
"extension": "mpd",
"abr_algorithm": "random"
"name": "Congo (360 top-bottom stereo)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
"spherical_stereo_mode": "top_bottom"
},
{
"name": "Sphericalv2 (180 top-bottom stereo)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
"spherical_stereo_mode": "top_bottom"
},
{
"name": "Iceland (360 top-bottom stereo ts)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
"spherical_stereo_mode": "top_bottom"
}
]
}
......
......@@ -16,19 +16,13 @@
package com.google.android.exoplayer2.demo;
import android.app.Application;
import com.google.android.exoplayer2.offline.DownloadAction.Deserializer;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.ProgressiveDownloadAction;
import com.google.android.exoplayer2.source.dash.offline.DashDownloadAction;
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction;
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
......@@ -46,13 +40,6 @@ public class DemoApplication extends Application {
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
private static final Deserializer[] DOWNLOAD_DESERIALIZERS =
new Deserializer[] {
DashDownloadAction.DESERIALIZER,
HlsDownloadAction.DESERIALIZER,
SsDownloadAction.DESERIALIZER,
ProgressiveDownloadAction.DESERIALIZER
};
protected String userAgent;
......@@ -68,16 +55,15 @@ public class DemoApplication extends Application {
}
/** Returns a {@link DataSource.Factory}. */
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
public DataSource.Factory buildDataSourceFactory() {
DefaultDataSourceFactory upstreamFactory =
new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
new DefaultDataSourceFactory(this, buildHttpDataSourceFactory());
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());
}
/** Returns a {@link HttpDataSource.Factory}. */
public HttpDataSource.Factory buildHttpDataSourceFactory(
TransferListener<? super DataSource> listener) {
return new DefaultHttpDataSourceFactory(userAgent, listener);
public HttpDataSource.Factory buildHttpDataSourceFactory() {
return new DefaultHttpDataSourceFactory(userAgent);
}
/** Returns whether extension renderers should be used. */
......@@ -98,21 +84,18 @@ public class DemoApplication extends Application {
private synchronized void initDownloadManager() {
if (downloadManager == null) {
DownloaderConstructorHelper downloaderConstructorHelper =
new DownloaderConstructorHelper(
getDownloadCache(), buildHttpDataSourceFactory(/* listener= */ null));
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager =
new DownloadManager(
downloaderConstructorHelper,
MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
DOWNLOAD_DESERIALIZERS);
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE));
downloadTracker =
new DownloadTracker(
/* context= */ this,
buildDataSourceFactory(/* listener= */ null),
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE),
DOWNLOAD_DESERIALIZERS);
buildDataSourceFactory(),
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
downloadManager.addListener(downloadTracker);
}
}
......
......@@ -22,7 +22,6 @@ import android.content.DialogInterface;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
......@@ -36,7 +35,7 @@ import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
import com.google.android.exoplayer2.offline.SegmentDownloadAction;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.offline.TrackKey;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
......@@ -46,6 +45,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHe
import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
import com.google.android.exoplayer2.ui.TrackNameProvider;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
......@@ -85,7 +85,7 @@ public class DownloadTracker implements DownloadManager.Listener {
Context context,
DataSource.Factory dataSourceFactory,
File actionFile,
DownloadAction.Deserializer[] deserializers) {
DownloadAction.Deserializer... deserializers) {
this.context = context.getApplicationContext();
this.dataSourceFactory = dataSourceFactory;
this.actionFile = new ActionFile(actionFile);
......@@ -95,7 +95,8 @@ public class DownloadTracker implements DownloadManager.Listener {
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
actionFileWriteThread.start();
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
loadTrackedActions(deserializers);
loadTrackedActions(
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers());
}
public void addListener(Listener listener) {
......@@ -111,15 +112,11 @@ public class DownloadTracker implements DownloadManager.Listener {
}
@SuppressWarnings("unchecked")
public <K> List<K> getOfflineStreamKeys(Uri uri) {
public List<StreamKey> getOfflineStreamKeys(Uri uri) {
if (!trackedDownloadStates.containsKey(uri)) {
return Collections.emptyList();
}
DownloadAction action = trackedDownloadStates.get(uri);
if (action instanceof SegmentDownloadAction) {
return ((SegmentDownloadAction) action).keys;
}
return Collections.emptyList();
return trackedDownloadStates.get(uri).getKeys();
}
public void toggleDownload(Activity activity, String name, Uri uri, String extension) {
......@@ -178,14 +175,11 @@ public class DownloadTracker implements DownloadManager.Listener {
}
final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]);
actionFileWriteHandler.post(
new Runnable() {
@Override
public void run() {
try {
actionFile.store(actions);
} catch (IOException e) {
Log.e(TAG, "Failed to store tracked actions", e);
}
() -> {
try {
actionFile.store(actions);
} catch (IOException e) {
Log.e(TAG, "Failed to store tracked actions", e);
}
});
}
......@@ -270,11 +264,11 @@ public class DownloadTracker implements DownloadManager.Listener {
trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
}
}
if (!trackKeys.isEmpty()) {
builder.setView(dialogView);
}
builder.create().show();
}
if (!trackKeys.isEmpty()) {
builder.setView(dialogView);
}
builder.create().show();
}
@Override
......@@ -282,6 +276,7 @@ public class DownloadTracker implements DownloadManager.Listener {
Toast.makeText(
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Failed to start download", e);
}
@Override
......
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/prefer_extension_decoders"
android:title="@string/prefer_extension_decoders"
android:showAsAction="never"
android:checkable="true"/>
<item android:id="@+id/random_abr"
android:title="@string/random_abr"
android:showAsAction="never"
android:checkable="true"/>
</menu>
......@@ -19,10 +19,14 @@
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
<string name="error_generic">Playback failed</string>
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string>
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
<string name="error_drm_not_supported">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>
......@@ -57,4 +61,8 @@
<string name="download_ads_unsupported">IMA does not support offline ads</string>
<string name="prefer_extension_decoders">Prefer extension decoders</string>
<string name="random_abr">Enable random ABR</string>
</resources>
......@@ -20,4 +20,8 @@
<item name="android:windowBackground">@android:color/black</item>
</style>
<style name="PlayerTheme.Spherical">
<item name="surface_type">spherical_view</item>
</style>
</resources>
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 14
targetSdkVersion project.ext.targetSdkVersion
......@@ -26,7 +31,7 @@ android {
}
dependencies {
api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
api 'com.google.android.gms:play-services-cast-framework:16.0.1'
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
testImplementation project(modulePrefix + 'testutils')
......
......@@ -15,9 +15,9 @@
*/
package com.google.android.exoplayer2.ext.cast;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
......@@ -29,6 +29,7 @@ import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.android.gms.cast.CastStatusCodes;
......@@ -284,6 +285,11 @@ public final class CastPlayer implements Player {
// Player implementation.
@Override
public AudioComponent getAudioComponent() {
return null;
}
@Override
public VideoComponent getVideoComponent() {
return null;
}
......@@ -294,6 +300,11 @@ public final class CastPlayer implements Player {
}
@Override
public Looper getApplicationLooper() {
return Looper.getMainLooper();
}
@Override
public void addListener(EventListener listener) {
listeners.add(listener);
}
......@@ -490,7 +501,7 @@ public final class CastPlayer implements Player {
@Override
public @Nullable Object getCurrentTag() {
int windowIndex = getCurrentWindowIndex();
return windowIndex > currentTimeline.getWindowCount()
return windowIndex >= currentTimeline.getWindowCount()
? null
: currentTimeline.getWindow(windowIndex, window, /* setTag= */ true).tag;
}
......@@ -527,6 +538,15 @@ public final class CastPlayer implements Player {
}
@Override
public long getTotalBufferedDuration() {
long bufferedPosition = getBufferedPosition();
long currentPosition = getCurrentPosition();
return bufferedPosition == C.TIME_UNSET || currentPosition == C.TIME_UNSET
? 0
: bufferedPosition - currentPosition;
}
@Override
public boolean isCurrentWindowDynamic() {
return !currentTimeline.isEmpty()
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
......@@ -554,6 +574,11 @@ public final class CastPlayer implements Player {
}
@Override
public long getContentDuration() {
return getDuration();
}
@Override
public boolean isLoading() {
return false;
}
......@@ -563,6 +588,11 @@ public final class CastPlayer implements Player {
return getCurrentPosition();
}
@Override
public long getContentBufferedPosition() {
return getBufferedPosition();
}
// Internal methods.
public void updateInternalState() {
......@@ -820,7 +850,6 @@ public final class CastPlayer implements Player {
@Override
public void onAdBreakStatusUpdated() {}
// SessionManagerListener implementation.
@Override
......
......@@ -32,8 +32,7 @@ import java.util.Map;
/* package */ final class CastTimeline extends Timeline {
public static final CastTimeline EMPTY_CAST_TIMELINE =
new CastTimeline(
Collections.<MediaQueueItem>emptyList(), Collections.<String, Long>emptyMap());
new CastTimeline(Collections.emptyList(), Collections.emptyMap());
private final SparseIntArray idsToIndex;
private final int[] ids;
......@@ -108,6 +107,11 @@ import java.util.Map;
return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
}
@Override
public Object getUidOfPeriod(int periodIndex) {
return ids[periodIndex];
}
// equals and hashCode implementations.
@Override
......
......@@ -101,8 +101,15 @@ import com.google.android.gms.cast.MediaTrack;
* @return The equivalent {@link Format}.
*/
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
return Format.createContainerFormat(
mediaTrack.getContentId(),
/* label= */ null,
mediaTrack.getContentType(),
/* sampleMimeType= */ null,
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
/* selectionFlags= */ 0,
mediaTrack.getLanguage());
}
private CastUtils() {}
......
......@@ -5,37 +5,22 @@ The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
## Build instructions ##
## Getting the extension ##
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 get the Cronet libraries
and enable the extension:
The easiest way to use the extension is to add it as a gradle dependency:
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
directory
1. Download `cronet_api.jar`, `cronet_impl_common_java.jar`,
`cronet_impl_native_java.jar` and the `libs` directory
1. Copy the three jar files into the `libs` directory of this extension
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
directory of this extension
1. In your `settings.gradle` file, add
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
applies `core_settings.gradle`.
1. In all `build.gradle` files where this extension is linked as a dependency,
add
```
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
```
to enable Java 8 features required by the Cronet library.
```gradle
implementation 'com.google.android.exoplayer:extension-cronet:2.X.X'
```
where `2.X.X` is the version, which must match the version of the ExoPlayer
library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module
locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
## Using the extension ##
......
......@@ -19,14 +19,10 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion project.ext.minSdkVersion
minSdkVersion 16
targetSdkVersion project.ext.targetSdkVersion
}
sourceSets.main {
jniLibs.srcDirs = ['jniLibs']
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
......@@ -34,9 +30,7 @@ android {
}
dependencies {
api files('libs/cronet_api.jar')
implementation files('libs/cronet_impl_common_java.jar')
implementation files('libs/cronet_impl_native_java.jar')
api 'org.chromium.net:cronet-embedded:66.3359.158'
implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
testImplementation project(modulePrefix + 'library')
......@@ -47,3 +41,9 @@ ext {
javadocTitle = 'Cronet extension'
}
apply from: '../../javadoc_library.gradle'
ext {
releaseArtifact = 'extension-cronet'
releaseDescription = 'Cronet extension for ExoPlayer.'
}
apply from: '../../publish.gradle'
Copy folders containing architecture specific .so files here.
Copy cronet.jar and cronet_api.jar here.
......@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.cronet;
import android.content.Context;
import android.support.annotation.IntDef;
import android.util.Log;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
......@@ -39,7 +40,8 @@ public final class CronetEngineWrapper {
private final @CronetEngineSource int cronetEngineSource;
/**
* Source of {@link CronetEngine}.
* Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link
* #SOURCE_UNKNOWN}, {@link #SOURCE_USER_PROVIDED} or {@link #SOURCE_UNAVAILABLE}.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})
......@@ -158,6 +160,8 @@ public final class CronetEngineWrapper {
private final String gmsCoreCronetName;
private final boolean preferGMSCoreCronet;
// Multi-catch can only be used for API 19+ in this case.
@SuppressWarnings("UseMultiCatch")
public CronetProviderComparator(boolean preferGMSCoreCronet) {
// GMSCore CronetProvider classes are only available in some configurations.
// Thus, we use reflection to copy static name.
......@@ -218,8 +222,8 @@ public final class CronetEngineWrapper {
if (versionLeft == null || versionRight == null) {
return 0;
}
String[] versionStringsLeft = versionLeft.split("\\.");
String[] versionStringsRight = versionRight.split("\\.");
String[] versionStringsLeft = Util.split(versionLeft, "\\.");
String[] versionStringsRight = Util.split(versionRight, "\\.");
int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);
for (int i = 0; i < minLength; i++) {
if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
......
......@@ -60,7 +60,7 @@ public final class ByteArrayUploadDataProviderTest {
@Test
public void testReadPartialBuffer() throws IOException {
byte[] firstHalf = Arrays.copyOfRange(TEST_DATA, 0, TEST_DATA.length / 2);
byte[] firstHalf = Arrays.copyOf(TEST_DATA, TEST_DATA.length / 2);
byte[] secondHalf = Arrays.copyOfRange(TEST_DATA, TEST_DATA.length / 2, TEST_DATA.length);
byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);
// Read half of the data.
......
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
......@@ -32,6 +37,8 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
}
ext {
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.ffmpeg;
import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
......@@ -26,29 +27,27 @@ import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Collections;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Decodes and renders audio using FFmpeg.
*/
public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
/**
* The number of input and output buffers.
*/
/** The number of input and output buffers. */
private static final int NUM_BUFFERS = 16;
/**
* The initial input buffer size. Input buffers are reallocated dynamically if this value is
* insufficient.
*/
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
/** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
private final boolean enableFloatOutput;
private FfmpegDecoder decoder;
private @MonotonicNonNull FfmpegDecoder decoder;
public FfmpegAudioRenderer() {
this(null, null);
this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
......@@ -57,9 +56,15 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
public FfmpegAudioRenderer(
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) {
this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false);
this(
eventHandler,
eventListener,
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors),
/* enableFloatOutput= */ false);
}
/**
......@@ -72,8 +77,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* 32-bit float output, any audio processing will be disabled, including playback speed/pitch
* adjustment.
*/
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioSink audioSink, boolean enableFloatOutput) {
public FfmpegAudioRenderer(
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioSink audioSink,
boolean enableFloatOutput) {
super(
eventHandler,
eventListener,
......@@ -86,10 +94,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager,
Format format) {
String sampleMimeType = format.sampleMimeType;
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) {
Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) {
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding)
|| !isOutputSupported(format)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
......@@ -106,18 +115,33 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException {
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format));
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
decoder =
new FfmpegDecoder(
NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));
return decoder;
}
@Override
public Format getOutputFormat() {
Assertions.checkNotNull(decoder);
int channelCount = decoder.getChannelCount();
int sampleRate = decoder.getSampleRate();
@C.PcmEncoding int encoding = decoder.getEncoding();
return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null);
return Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
Format.NO_VALUE,
Format.NO_VALUE,
channelCount,
sampleRate,
encoding,
Collections.emptyList(),
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
}
private boolean isOutputSupported(Format inputFormat) {
......@@ -125,6 +149,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
private boolean shouldUseFloatOutput(Format inputFormat) {
Assertions.checkNotNull(inputFormat.sampleMimeType);
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
return false;
}
......
......@@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer;
......@@ -30,13 +33,12 @@ import java.util.List;
/* package */ final class FfmpegDecoder extends
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FfmpegDecoderException> {
// Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio.
private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2;
// Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio.
// Output buffer sizes when decoding PCM mu-law streams, which is the maximum FFmpeg outputs.
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
private final String codecName;
private final byte[] extraData;
private final @Nullable byte[] extraData;
private final @C.Encoding int encoding;
private final int outputBufferSize;
......@@ -45,18 +47,26 @@ import java.util.List;
private volatile int channelCount;
private volatile int sampleRate;
public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
String mimeType, List<byte[]> initializationData, boolean outputFloat)
public FfmpegDecoder(
int numInputBuffers,
int numOutputBuffers,
int initialInputBufferSize,
Format format,
boolean outputFloat)
throws FfmpegDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!FfmpegLibrary.isAvailable()) {
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
}
codecName = FfmpegLibrary.getCodecName(mimeType);
extraData = getExtraData(mimeType, initializationData);
Assertions.checkNotNull(format.sampleMimeType);
codecName =
Assertions.checkNotNull(
FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));
extraData = getExtraData(format.sampleMimeType, format.initializationData);
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
nativeContext = ffmpegInitialize(codecName, extraData, outputFloat);
nativeContext =
ffmpegInitialize(codecName, extraData, outputFloat, format.sampleRate, format.channelCount);
if (nativeContext == 0) {
throw new FfmpegDecoderException("Initialization failed.");
}
......@@ -84,7 +94,7 @@ import java.util.List;
}
@Override
protected FfmpegDecoderException decode(
protected @Nullable FfmpegDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
nativeContext = ffmpegReset(nativeContext, extraData);
......@@ -103,6 +113,7 @@ import java.util.List;
channelCount = ffmpegGetChannelCount(nativeContext);
sampleRate = ffmpegGetSampleRate(nativeContext);
if (sampleRate == 0 && "alac".equals(codecName)) {
Assertions.checkNotNull(extraData);
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
// See https://trac.ffmpeg.org/ticket/6096
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
......@@ -148,7 +159,7 @@ import java.util.List;
* Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if
* not required.
*/
private static byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
case MimeTypes.AUDIO_ALAC:
......@@ -173,12 +184,20 @@ import java.util.List;
}
}
private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat);
private native long ffmpegInitialize(
String codecName,
@Nullable byte[] extraData,
boolean outputFloat,
int rawSampleRate,
int rawChannelCount);
private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize,
ByteBuffer outputData, int outputSize);
private native int ffmpegGetChannelCount(long context);
private native int ffmpegGetSampleRate(long context);
private native long ffmpegReset(long context, byte[] extraData);
private native long ffmpegReset(long context, @Nullable byte[] extraData);
private native void ffmpegRelease(long context);
}
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.ffmpeg;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.MimeTypes;
......@@ -51,10 +53,8 @@ public final class FfmpegLibrary {
return LOADER.isAvailable();
}
/**
* Returns the version of the underlying library if available, or null otherwise.
*/
public static String getVersion() {
/** Returns the version of the underlying library if available, or null otherwise. */
public static @Nullable String getVersion() {
return isAvailable() ? ffmpegGetVersion() : null;
}
......@@ -62,19 +62,21 @@ public final class FfmpegLibrary {
* Returns whether the underlying library supports the specified MIME type.
*
* @param mimeType The MIME type to check.
* @param encoding The PCM encoding for raw audio.
*/
public static boolean supportsFormat(String mimeType) {
public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) {
if (!isAvailable()) {
return false;
}
String codecName = getCodecName(mimeType);
String codecName = getCodecName(mimeType, encoding);
return codecName != null && ffmpegHasDecoder(codecName);
}
/**
* Returns the name of the FFmpeg decoder that could be used to decode {@code mimeType}.
* Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
* if it's unsupported.
*/
/* package */ static String getCodecName(String mimeType) {
/* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
return "aac";
......@@ -85,6 +87,7 @@ public final class FfmpegLibrary {
case MimeTypes.AUDIO_AC3:
return "ac3";
case MimeTypes.AUDIO_E_AC3:
case MimeTypes.AUDIO_E_AC3_JOC:
return "eac3";
case MimeTypes.AUDIO_TRUEHD:
return "truehd";
......@@ -103,6 +106,14 @@ public final class FfmpegLibrary {
return "flac";
case MimeTypes.AUDIO_ALAC:
return "alac";
case MimeTypes.AUDIO_RAW:
if (encoding == C.ENCODING_PCM_MU_LAW) {
return "pcm_mulaw";
} else if (encoding == C.ENCODING_PCM_A_LAW) {
return "pcm_alaw";
} else {
return null;
}
default:
return null;
}
......
......@@ -27,6 +27,7 @@ extern "C" {
#endif
#include <libavcodec/avcodec.h>
#include <libavresample/avresample.h>
#include <libavutil/channel_layout.h>
#include <libavutil/error.h>
#include <libavutil/opt.h>
}
......@@ -72,8 +73,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName);
* provided extraData as initialization data for the decoder if it is non-NULL.
* Returns the created context.
*/
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
jbyteArray extraData, jboolean outputFloat);
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
jboolean outputFloat, jint rawSampleRate,
jint rawChannelCount);
/**
* Decodes the packet into the output buffer, returning the number of bytes
......@@ -110,13 +112,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
}
DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData,
jboolean outputFloat) {
jboolean outputFloat, jint rawSampleRate, jint rawChannelCount) {
AVCodec *codec = getCodecByName(env, codecName);
if (!codec) {
LOGE("Codec not found.");
return 0L;
}
return (jlong) createContext(env, codec, extraData, outputFloat);
return (jlong)createContext(env, codec, extraData, outputFloat, rawSampleRate,
rawChannelCount);
}
DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData,
......@@ -180,8 +183,11 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) {
LOGE("Unexpected error finding codec %d.", codecId);
return 0L;
}
return (jlong) createContext(env, codec, extraData,
context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
jboolean outputFloat =
(jboolean)(context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT);
return (jlong)createContext(env, codec, extraData, outputFloat,
/* rawSampleRate= */ -1,
/* rawChannelCount= */ -1);
}
avcodec_flush_buffers(context);
......@@ -204,8 +210,9 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) {
return codec;
}
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
jbyteArray extraData, jboolean outputFloat) {
AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
jboolean outputFloat, jint rawSampleRate,
jint rawChannelCount) {
AVCodecContext *context = avcodec_alloc_context3(codec);
if (!context) {
LOGE("Failed to allocate context.");
......@@ -225,6 +232,12 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec,
}
env->GetByteArrayRegion(extraData, 0, size, (jbyte *) context->extradata);
}
if (context->codec_id == AV_CODEC_ID_PCM_MULAW ||
context->codec_id == AV_CODEC_ID_PCM_ALAW) {
context->sample_rate = rawSampleRate;
context->channels = rawChannelCount;
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
}
int result = avcodec_open2(context, codec, NULL);
if (result < 0) {
logError("avcodec_open2", result);
......
......@@ -18,10 +18,16 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
sourceSets.main {
......@@ -33,6 +39,7 @@ android {
dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core')
androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
androidTestImplementation project(modulePrefix + 'testutils')
testImplementation project(modulePrefix + 'testutils-robolectric')
}
......
......@@ -26,6 +26,6 @@
<instrumentation
android:targetPackage="com.google.android.exoplayer2.ext.flac.test"
android:name="android.test.InstrumentationTestRunner"/>
android:name="androidx.test.runner.AndroidJUnitRunner"/>
</manifest>
......@@ -67,6 +67,6 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.hasPendingSeek()).isTrue();
assertThat(seeker.isSeeking()).isTrue();
}
}
......@@ -16,9 +16,7 @@
package com.google.android.exoplayer2.ext.flac;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/**
* Unit test for {@link FlacExtractor}.
......@@ -35,25 +33,11 @@ public class FlacExtractorTest extends InstrumentationTestCase {
public void testExtractFlacSample() throws Exception {
ExtractorAsserts.assertBehavior(
new ExtractorFactory() {
@Override
public Extractor create() {
return new FlacExtractor();
}
},
"bear.flac",
getInstrumentation().getContext());
FlacExtractor::new, "bear.flac", getInstrumentation().getContext());
}
public void testExtractFlacSampleWithId3Header() throws Exception {
ExtractorAsserts.assertBehavior(
new ExtractorFactory() {
@Override
public Extractor create() {
return new FlacExtractor();
}
},
"bear_with_id3.flac",
getInstrumentation().getContext());
FlacExtractor::new, "bear_with_id3.flac", getInstrumentation().getContext());
}
}
......@@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2.ext.flac;
import static androidx.test.InstrumentationRegistry.getContext;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
......@@ -29,43 +32,40 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Playback tests using {@link LibflacAudioRenderer}.
*/
public class FlacPlaybackTest extends InstrumentationTestCase {
/** Playback tests using {@link LibflacAudioRenderer}. */
@RunWith(AndroidJUnit4.class)
public class FlacPlaybackTest {
private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka";
@Override
protected void setUp() throws Exception {
super.setUp();
@Before
public void setUp() {
if (!FlacLibrary.isAvailable()) {
fail("Flac library not available.");
}
}
public void testBasicPlayback() throws ExoPlaybackException {
@Test
public void testBasicPlayback() throws Exception {
playUri(BEAR_FLAC_URI);
}
private void playUri(String uri) throws ExoPlaybackException {
TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri),
getInstrumentation().getContext());
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(Uri.parse(uri), getContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
fail(); // Should never happen.
}
thread.join();
if (testPlaybackRunnable.playbackException != null) {
throw testPlaybackRunnable.playbackException;
}
}
private static class TestPlaybackRunnable extends Player.DefaultEventListener
implements Runnable {
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
private final Context context;
private final Uri uri;
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.flac;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
......@@ -37,11 +38,17 @@ import java.util.List;
*
* @param numInputBuffers The number of input buffers.
* @param numOutputBuffers The number of output buffers.
* @param maxInputBufferSize The maximum required input buffer size if known, or {@link
* Format#NO_VALUE} otherwise.
* @param initializationData Codec-specific initialization data. It should contain only one entry
* which is the flac file header.
* which is the flac file header.
* @throws FlacDecoderException Thrown if an exception occurs when initializing the decoder.
*/
public FlacDecoder(int numInputBuffers, int numOutputBuffers, List<byte[]> initializationData)
public FlacDecoder(
int numInputBuffers,
int numOutputBuffers,
int maxInputBufferSize,
List<byte[]> initializationData)
throws FlacDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (initializationData.size() != 1) {
......@@ -60,7 +67,9 @@ import java.util.List;
throw new FlacDecoderException("Metadata decoding failed");
}
setInitialInputBufferSize(streamInfo.maxFrameSize);
int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamInfo.maxDecodedFrameSize();
}
......
......@@ -155,6 +155,7 @@ import java.nio.ByteBuffer;
}
/** Decodes and consumes the next sample from the FLAC stream into the given byte buffer. */
@SuppressWarnings("ByteBufferBackingArray")
public void decodeSample(ByteBuffer output)
throws IOException, InterruptedException, FlacFrameDecodeException {
output.clear();
......
......@@ -21,6 +21,7 @@ import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
......@@ -46,24 +47,17 @@ import java.util.Arrays;
*/
public final class FlacExtractor implements Extractor {
/** Factory that returns one extractor which is a {@link FlacExtractor}. */
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
/**
* Factory that returns one extractor which is a {@link FlacExtractor}.
* Flags controlling the behavior of the extractor. Possible flag value is {@link
* #FLAG_DISABLE_ID3_METADATA}.
*/
public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
@Override
public Extractor[] createExtractors() {
return new Extractor[] {new FlacExtractor()};
}
};
/** Flags controlling the behavior of the extractor. */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {FLAG_DISABLE_ID3_METADATA}
)
flag = true,
value = {FLAG_DISABLE_ID3_METADATA})
public @interface Flags {}
/**
......@@ -88,6 +82,7 @@ public final class FlacExtractor implements Extractor {
private ParsableByteArray outputBuffer;
private ByteBuffer outputByteBuffer;
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
private FlacStreamInfo streamInfo;
private Metadata id3Metadata;
......@@ -140,7 +135,7 @@ public final class FlacExtractor implements Extractor {
decoderJni.setData(input);
readPastStreamInfo(input);
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.hasPendingSeek()) {
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
return handlePendingSeek(input, seekPosition);
}
......@@ -224,6 +219,7 @@ public final class FlacExtractor implements Extractor {
outputFormat(streamInfo);
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
}
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
......@@ -286,9 +282,10 @@ public final class FlacExtractor implements Extractor {
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
throws InterruptedException, IOException {
int seekResult =
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputByteBuffer);
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
writeLastSampleToOutput(outputByteBuffer.limit(), decoderJni.getLastFrameTimestamp());
writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
}
return seekResult;
}
......
......@@ -65,7 +65,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FlacDecoderException {
return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData);
return new FlacDecoder(
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest package="com.google.android.exoplayer2.ext.flac"/>
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.extractor.wav.WavExtractor;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Unit test for {@link DefaultExtractorsFactory}. */
@RunWith(RobolectricTestRunner.class)
public final class DefaultExtractorsFactoryTest {
@Test
public void testCreateExtractors_returnExpectedClasses() {
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
Extractor[] extractors = defaultExtractorsFactory.createExtractors();
List<Class<?>> listCreatedExtractorClasses = new ArrayList<>();
for (Extractor extractor : extractors) {
listCreatedExtractorClasses.add(extractor.getClass());
}
Class<?>[] expectedExtractorClassses =
new Class<?>[] {
MatroskaExtractor.class,
FragmentedMp4Extractor.class,
Mp4Extractor.class,
Mp3Extractor.class,
AdtsExtractor.class,
Ac3Extractor.class,
TsExtractor.class,
FlvExtractor.class,
OggExtractor.class,
PsExtractor.class,
WavExtractor.class,
AmrExtractor.class,
FlacExtractor.class
};
assertThat(listCreatedExtractorClasses).containsNoDuplicates();
assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses);
}
}
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
......
......@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.util.Assertions;
import com.google.vr.sdk.audio.GvrAudioSurround;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
......@@ -148,18 +149,21 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public void queueInput(ByteBuffer input) {
int position = input.position();
Assertions.checkNotNull(gvrAudioSurround);
int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);
input.position(position + readBytes);
}
@Override
public void queueEndOfStream() {
Assertions.checkNotNull(gvrAudioSurround);
inputEnded = true;
gvrAudioSurround.triggerProcessing();
}
@Override
public ByteBuffer getOutput() {
Assertions.checkNotNull(gvrAudioSurround);
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
buffer.position(0).limit(writtenBytes);
return buffer;
......@@ -167,6 +171,7 @@ public final class GvrAudioProcessor implements AudioProcessor {
@Override
public boolean isEnded() {
Assertions.checkNotNull(gvrAudioSurround);
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
}
......
......@@ -30,7 +30,9 @@ To play ads alongside a single-window content `MediaSource`, prepare the player
with an `AdsMediaSource` constructed using an `ImaAdsLoader`, the content
`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag
URI from your ad campaign when creating the `ImaAdsLoader`. The IMA
documentation includes some [sample ad tags][] for testing.
documentation includes some [sample ad tags][] for testing. Note that the IMA
extension only supports players which are accessed on the application's main
thread.
Resuming the player after entering the background requires some special handling
when playing ads. The player and its media source are released on entering the
......
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
......@@ -26,9 +31,9 @@ android {
}
dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.7'
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.9.4'
implementation project(modulePrefix + 'library-core')
implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion
implementation 'com.google.android.gms:play-services-ads:15.0.1'
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4 and com.android.support:customtabs to be
// used. Else older versions are used, for example via:
......@@ -36,6 +41,11 @@ dependencies {
// |-- com.android.support:customtabs:26.1.0
implementation 'com.android.support:support-v4:' + supportLibraryVersion
implementation 'com.android.support:customtabs:' + supportLibraryVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
}
ext {
......
......@@ -23,9 +23,11 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
/**
......@@ -34,12 +36,10 @@ import java.io.IOException;
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
*/
@Deprecated
public final class ImaAdsMediaSource extends BaseMediaSource {
public final class ImaAdsMediaSource extends BaseMediaSource implements SourceInfoRefreshListener {
private final AdsMediaSource adsMediaSource;
private SourceInfoRefreshListener adsMediaSourceListener;
/**
* Constructs a new source that inserts ads linearly with the content specified by
* {@code contentMediaSource}.
......@@ -77,16 +77,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
}
@Override
public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
adsMediaSourceListener =
new SourceInfoRefreshListener() {
@Override
public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, @Nullable Object manifest) {
refreshSourceInfo(timeline, manifest);
}
};
adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
public void prepareSourceInternal(
final ExoPlayer player,
boolean isTopLevelSource,
@Nullable TransferListener mediaTransferListener) {
adsMediaSource.prepareSource(
player, isTopLevelSource, /* listener= */ this, mediaTransferListener);
}
@Override
......@@ -106,6 +102,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource {
@Override
public void releaseSourceInternal() {
adsMediaSource.releaseSource(adsMediaSourceListener);
adsMediaSource.releaseSource(/* listener= */ this);
}
@Override
public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, @Nullable Object manifest) {
refreshSourceInfo(timeline, manifest);
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest package="com.google.android.exoplayer2.ext.ima.test" />
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ima;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdPodInfo;
import com.google.ads.interactivemedia.v3.api.CompanionAd;
import com.google.ads.interactivemedia.v3.api.UiElement;
import java.util.List;
import java.util.Set;
/** A fake ad for testing. */
/* package */ final class FakeAd implements Ad {
private final boolean skippable;
private final AdPodInfo adPodInfo;
public FakeAd(boolean skippable, int podIndex, int totalAds, int adPosition) {
this.skippable = skippable;
adPodInfo =
new AdPodInfo() {
@Override
public int getTotalAds() {
return totalAds;
}
@Override
public int getAdPosition() {
return adPosition;
}
@Override
public int getPodIndex() {
return podIndex;
}
@Override
public boolean isBumper() {
throw new UnsupportedOperationException();
}
@Override
public double getMaxDuration() {
throw new UnsupportedOperationException();
}
@Override
public double getTimeOffset() {
throw new UnsupportedOperationException();
}
};
}
public int getVastMediaWidth() {
throw new UnsupportedOperationException();
}
public int getVastMediaHeight() {
throw new UnsupportedOperationException();
}
public int getVastMediaBitrate() {
throw new UnsupportedOperationException();
}
@Override
public boolean isSkippable() {
return skippable;
}
@Override
public AdPodInfo getAdPodInfo() {
return adPodInfo;
}
@Override
public String getAdId() {
throw new UnsupportedOperationException();
}
@Override
public String getCreativeId() {
throw new UnsupportedOperationException();
}
@Override
public String getCreativeAdId() {
throw new UnsupportedOperationException();
}
@Override
public String getUniversalAdIdValue() {
throw new UnsupportedOperationException();
}
@Override
public String getUniversalAdIdRegistry() {
throw new UnsupportedOperationException();
}
@Override
public String getAdSystem() {
throw new UnsupportedOperationException();
}
@Override
public String[] getAdWrapperIds() {
throw new UnsupportedOperationException();
}
@Override
public String[] getAdWrapperSystems() {
throw new UnsupportedOperationException();
}
@Override
public String[] getAdWrapperCreativeIds() {
throw new UnsupportedOperationException();
}
@Override
public boolean isLinear() {
throw new UnsupportedOperationException();
}
@Override
public double getSkipTimeOffset() {
throw new UnsupportedOperationException();
}
@Override
public boolean isUiDisabled() {
throw new UnsupportedOperationException();
}
@Override
public String getDescription() {
throw new UnsupportedOperationException();
}
@Override
public String getTitle() {
throw new UnsupportedOperationException();
}
@Override
public String getContentType() {
throw new UnsupportedOperationException();
}
@Override
public String getAdvertiserName() {
throw new UnsupportedOperationException();
}
@Override
public String getSurveyUrl() {
throw new UnsupportedOperationException();
}
@Override
public String getDealId() {
throw new UnsupportedOperationException();
}
@Override
public int getWidth() {
throw new UnsupportedOperationException();
}
@Override
public int getHeight() {
throw new UnsupportedOperationException();
}
@Override
public String getTraffickingParameters() {
throw new UnsupportedOperationException();
}
@Override
public double getDuration() {
throw new UnsupportedOperationException();
}
@Override
public Set<UiElement> getUiElements() {
throw new UnsupportedOperationException();
}
@Override
public List<CompanionAd> getCompanionAds() {
throw new UnsupportedOperationException();
}
}
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ima;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.StreamManager;
import com.google.ads.interactivemedia.v3.api.StreamRequest;
import com.google.android.exoplayer2.util.Assertions;
import java.util.ArrayList;
/** Fake {@link com.google.ads.interactivemedia.v3.api.AdsLoader} implementation for tests. */
public final class FakeAdsLoader implements com.google.ads.interactivemedia.v3.api.AdsLoader {
private final ImaSdkSettings imaSdkSettings;
private final AdsManager adsManager;
private final ArrayList<AdsLoadedListener> adsLoadedListeners;
private final ArrayList<AdErrorListener> adErrorListeners;
public FakeAdsLoader(ImaSdkSettings imaSdkSettings, AdsManager adsManager) {
this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings);
this.adsManager = Assertions.checkNotNull(adsManager);
adsLoadedListeners = new ArrayList<>();
adErrorListeners = new ArrayList<>();
}
@Override
public void contentComplete() {
// Do nothing.
}
@Override
public ImaSdkSettings getSettings() {
return imaSdkSettings;
}
@Override
public void requestAds(AdsRequest adsRequest) {
for (AdsLoadedListener listener : adsLoadedListeners) {
listener.onAdsManagerLoaded(
new AdsManagerLoadedEvent() {
@Override
public AdsManager getAdsManager() {
return adsManager;
}
@Override
public StreamManager getStreamManager() {
throw new UnsupportedOperationException();
}
@Override
public Object getUserRequestContext() {
return adsRequest.getUserRequestContext();
}
});
}
}
@Override
public String requestStream(StreamRequest streamRequest) {
throw new UnsupportedOperationException();
}
@Override
public void addAdsLoadedListener(AdsLoadedListener adsLoadedListener) {
adsLoadedListeners.add(adsLoadedListener);
}
@Override
public void removeAdsLoadedListener(AdsLoadedListener adsLoadedListener) {
adsLoadedListeners.remove(adsLoadedListener);
}
@Override
public void addAdErrorListener(AdErrorListener adErrorListener) {
adErrorListeners.add(adErrorListener);
}
@Override
public void removeAdErrorListener(AdErrorListener adErrorListener) {
adErrorListeners.remove(adErrorListener);
}
}
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ima;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
import java.util.List;
import java.util.Map;
/** Fake {@link AdsRequest} implementation for tests. */
public final class FakeAdsRequest implements AdsRequest {
private String adTagUrl;
private String adsResponse;
private Object userRequestContext;
private AdDisplayContainer adDisplayContainer;
private ContentProgressProvider contentProgressProvider;
@Override
public void setAdTagUrl(String adTagUrl) {
this.adTagUrl = adTagUrl;
}
@Override
public String getAdTagUrl() {
return adTagUrl;
}
@Override
public void setExtraParameter(String s, String s1) {
throw new UnsupportedOperationException();
}
@Override
public String getExtraParameter(String s) {
throw new UnsupportedOperationException();
}
@Override
public Map<String, String> getExtraParameters() {
throw new UnsupportedOperationException();
}
@Override
public void setUserRequestContext(Object userRequestContext) {
this.userRequestContext = userRequestContext;
}
@Override
public Object getUserRequestContext() {
return userRequestContext;
}
@Override
public AdDisplayContainer getAdDisplayContainer() {
return adDisplayContainer;
}
@Override
public void setAdDisplayContainer(AdDisplayContainer adDisplayContainer) {
this.adDisplayContainer = adDisplayContainer;
}
@Override
public ContentProgressProvider getContentProgressProvider() {
return contentProgressProvider;
}
@Override
public void setContentProgressProvider(ContentProgressProvider contentProgressProvider) {
this.contentProgressProvider = contentProgressProvider;
}
@Override
public String getAdsResponse() {
return adsResponse;
}
@Override
public void setAdsResponse(String adsResponse) {
this.adsResponse = adsResponse;
}
@Override
public void setAdWillAutoPlay(boolean b) {
throw new UnsupportedOperationException();
}
@Override
public void setAdWillPlayMuted(boolean b) {
throw new UnsupportedOperationException();
}
@Override
public void setContentDuration(float v) {
throw new UnsupportedOperationException();
}
@Override
public void setContentKeywords(List<String> list) {
throw new UnsupportedOperationException();
}
@Override
public void setContentTitle(String s) {
throw new UnsupportedOperationException();
}
@Override
public void setVastLoadTimeout(float v) {
throw new UnsupportedOperationException();
}
@Override
public void setLiveStreamPrefetchSeconds(float v) {
throw new UnsupportedOperationException();
}
}
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ima;
import android.os.Looper;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.StubExoPlayer;
import java.util.ArrayList;
/** A fake player for testing content/ad playback. */
/* package */ final class FakePlayer extends StubExoPlayer {
private final ArrayList<Player.EventListener> listeners;
private final Timeline.Window window;
private final Timeline.Period period;
private final Timeline timeline;
private boolean prepared;
private int state;
private boolean playWhenReady;
private long position;
private long contentPosition;
private boolean isPlayingAd;
private int adGroupIndex;
private int adIndexInAdGroup;
public FakePlayer() {
listeners = new ArrayList<>();
window = new Timeline.Window();
period = new Timeline.Period();
state = Player.STATE_IDLE;
playWhenReady = true;
timeline = Timeline.EMPTY;
}
/** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */
public void updateTimeline(Timeline timeline) {
for (Player.EventListener listener : listeners) {
listener.onTimelineChanged(
timeline,
null,
prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
}
prepared = true;
}
/**
* Sets the state of this player as if it were playing content at the given {@code position}. If
* an ad is currently playing, this will trigger a position discontinuity.
*/
public void setPlayingContentPosition(long position) {
boolean notify = isPlayingAd;
isPlayingAd = false;
adGroupIndex = C.INDEX_UNSET;
adIndexInAdGroup = C.INDEX_UNSET;
this.position = position;
contentPosition = position;
if (notify) {
for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);
}
}
}
/**
* Sets the state of this player as if it were playing an ad with the given indices at the given
* {@code position}. If the player is playing a different ad or content, this will trigger a
* position discontinuity.
*/
public void setPlayingAdPosition(
int adGroupIndex, int adIndexInAdGroup, long position, long contentPosition) {
boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup;
isPlayingAd = true;
this.adGroupIndex = adGroupIndex;
this.adIndexInAdGroup = adIndexInAdGroup;
this.position = position;
this.contentPosition = contentPosition;
if (notify) {
for (Player.EventListener listener : listeners) {
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION);
}
}
}
/** Sets the state of this player with the given {@code STATE} constant. */
public void setState(int state, boolean playWhenReady) {
boolean notify = this.state != state || this.playWhenReady != playWhenReady;
this.state = state;
this.playWhenReady = playWhenReady;
if (notify) {
for (Player.EventListener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, state);
}
}
}
// ExoPlayer methods. Other methods are unsupported.
@Override
public Looper getApplicationLooper() {
return Looper.getMainLooper();
}
@Override
public void addListener(Player.EventListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(Player.EventListener listener) {
listeners.remove(listener);
}
@Override
public int getPlaybackState() {
return state;
}
@Override
public boolean getPlayWhenReady() {
return playWhenReady;
}
@Override
public Timeline getCurrentTimeline() {
return timeline;
}
@Override
public int getCurrentPeriodIndex() {
return 0;
}
@Override
public int getCurrentWindowIndex() {
return 0;
}
@Override
public int getNextWindowIndex() {
return C.INDEX_UNSET;
}
@Override
public int getPreviousWindowIndex() {
return C.INDEX_UNSET;
}
@Override
public long getDuration() {
if (timeline.isEmpty()) {
return C.INDEX_UNSET;
}
if (isPlayingAd()) {
long adDurationUs =
timeline.getPeriod(0, period).getAdDurationUs(adGroupIndex, adIndexInAdGroup);
return C.usToMs(adDurationUs);
} else {
return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
}
}
@Override
public long getCurrentPosition() {
return position;
}
@Override
public boolean isPlayingAd() {
return isPlayingAd;
}
@Override
public int getCurrentAdGroupIndex() {
return adGroupIndex;
}
@Override
public int getCurrentAdIndexInAdGroup() {
return adIndexInAdGroup;
}
@Override
public long getContentPosition() {
return contentPosition;
}
}
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ima;
import android.content.Context;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
/** {@link ImaAdsLoader.ImaFactory} that returns provided instances from each getter, for tests. */
final class SingletonImaFactory implements ImaAdsLoader.ImaFactory {
private final ImaSdkSettings imaSdkSettings;
private final AdsRenderingSettings adsRenderingSettings;
private final AdDisplayContainer adDisplayContainer;
private final AdsRequest adsRequest;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
public SingletonImaFactory(
ImaSdkSettings imaSdkSettings,
AdsRenderingSettings adsRenderingSettings,
AdDisplayContainer adDisplayContainer,
AdsRequest adsRequest,
com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) {
this.imaSdkSettings = imaSdkSettings;
this.adsRenderingSettings = adsRenderingSettings;
this.adDisplayContainer = adDisplayContainer;
this.adsRequest = adsRequest;
this.adsLoader = adsLoader;
}
@Override
public ImaSdkSettings createImaSdkSettings() {
return imaSdkSettings;
}
@Override
public AdsRenderingSettings createAdsRenderingSettings() {
return adsRenderingSettings;
}
@Override
public AdDisplayContainer createAdDisplayContainer() {
return adDisplayContainer;
}
@Override
public AdsRequest createAdsRequest() {
return adsRequest;
}
@Override
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings) {
return adsLoader;
}
}
......@@ -20,6 +20,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
......
......@@ -18,17 +18,17 @@ package com.google.android.exoplayer2.ext.jobdispatcher;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.firebase.jobdispatcher.Constraint;
import com.firebase.jobdispatcher.FirebaseJobDispatcher;
import com.firebase.jobdispatcher.GooglePlayDriver;
import com.firebase.jobdispatcher.Job;
import com.firebase.jobdispatcher.Job.Builder;
import com.firebase.jobdispatcher.JobParameters;
import com.firebase.jobdispatcher.JobService;
import com.firebase.jobdispatcher.Lifetime;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
/**
......@@ -37,6 +37,7 @@ import com.google.android.exoplayer2.util.Util;
*
* <pre>{@literal
* <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
* <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
*
* <service
* android:name="com.google.android.exoplayer2.ext.jobdispatcher.JobDispatcherScheduler$JobDispatcherSchedulerService"
......@@ -97,7 +98,7 @@ public final class JobDispatcherScheduler implements Scheduler {
String tag,
String serviceAction,
String servicePackage) {
Builder builder =
Job.Builder builder =
dispatcher
.newJobBuilder()
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
......@@ -146,11 +147,14 @@ public final class JobDispatcherScheduler implements Scheduler {
public boolean onStartJob(JobParameters params) {
logd("JobDispatcherSchedulerService is started");
Bundle extras = params.getExtras();
Assertions.checkNotNull(extras, "Service started without extras.");
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
if (requirements.checkRequirements(this)) {
logd("Requirements are met");
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
Assertions.checkNotNull(serviceAction, "Service action missing.");
Assertions.checkNotNull(servicePackage, "Service package missing.");
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
Util.startForegroundService(this, intent);
......
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 17
targetSdkVersion project.ext.targetSdkVersion
......
......@@ -39,7 +39,7 @@ import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.video.VideoListener;
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
public final class LeanbackPlayerAdapter extends PlayerAdapter {
public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnable {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.leanback");
......@@ -49,12 +49,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
private final Player player;
private final Handler handler;
private final ComponentListener componentListener;
private final Runnable updateProgressRunnable;
private final int updatePeriodMs;
private @Nullable PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private SurfaceHolderGlueHost surfaceHolderGlueHost;
private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface;
private boolean lastNotifiedPreparedState;
......@@ -70,18 +70,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {
this.context = context;
this.player = player;
this.updatePeriodMs = updatePeriodMs;
handler = new Handler();
componentListener = new ComponentListener();
controlDispatcher = new DefaultControlDispatcher();
updateProgressRunnable = new Runnable() {
@Override
public void run() {
Callback callback = getCallback();
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
handler.postDelayed(this, updatePeriodMs);
}
};
}
/**
......@@ -138,7 +130,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
videoComponent.removeVideoListener(componentListener);
}
if (surfaceHolderGlueHost != null) {
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
removeSurfaceHolderCallback(surfaceHolderGlueHost);
surfaceHolderGlueHost = null;
}
hasSurface = false;
......@@ -150,9 +142,9 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
@Override
public void setProgressUpdatingEnabled(boolean enabled) {
handler.removeCallbacks(updateProgressRunnable);
handler.removeCallbacks(this);
if (enabled) {
handler.post(updateProgressRunnable);
handler.post(this);
}
}
......@@ -211,9 +203,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
&& (surfaceHolderGlueHost == null || hasSurface);
}
// Runnable implementation.
@Override
public void run() {
Callback callback = getCallback();
callback.onCurrentPositionChanged(this);
callback.onBufferedPositionChanged(this);
handler.postDelayed(this, updatePeriodMs);
}
// Internal methods.
/* package */ void setVideoSurface(Surface surface) {
/* package */ void setVideoSurface(@Nullable Surface surface) {
hasSurface = surface != null;
Player.VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
......@@ -241,8 +243,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
}
private final class ComponentListener extends Player.DefaultEventListener
implements SurfaceHolder.Callback, VideoListener {
@SuppressWarnings("nullness:argument.type.incompatible")
private static void removeSurfaceHolderCallback(SurfaceHolderGlueHost surfaceHolderGlueHost) {
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
}
private final class ComponentListener
implements Player.EventListener, SurfaceHolder.Callback, VideoListener {
// SurfaceHolder.Callback implementation.
......
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
......
/*
* Copyright (c) 2017 The Android Open Source Project
* 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.
......@@ -127,7 +127,7 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback
@Override
public void onStop(Player player) {
player.stop();
player.stop(true);
}
@Override
......
/*
* Copyright (c) 2017 The Android Open Source Project
* 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.
......
/*
* Copyright (c) 2017 The Android Open Source Project
* 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.
......@@ -184,18 +184,13 @@ public final class TimelineQueueEditor
List<MediaSessionCompat.QueueItem> queue = mediaController.getQueue();
for (int i = 0; i < queue.size(); i++) {
if (equalityChecker.equals(queue.get(i).getDescription(), description)) {
onRemoveQueueItemAt(player, i);
queueDataAdapter.remove(i);
queueMediaSource.removeMediaSource(i);
return;
}
}
}
@Override
public void onRemoveQueueItemAt(Player player, int index) {
queueDataAdapter.remove(index);
queueMediaSource.removeMediaSource(index);
}
// CommandReceiver implementation.
@NonNull
......
......@@ -175,7 +175,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
private void publishFloatingQueueWindow(Player player) {
if (player.getCurrentTimeline().isEmpty()) {
mediaSession.setQueue(Collections.<MediaSessionCompat.QueueItem>emptyList());
mediaSession.setQueue(Collections.emptyList());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return;
}
......
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
......@@ -28,7 +33,8 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
api 'com.squareup.okhttp3:okhttp:3.10.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
api 'com.squareup.okhttp3:okhttp:3.11.0'
}
ext {
......
......@@ -15,9 +15,7 @@
*/
package com.google.android.exoplayer2.ext.okhttp;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
......@@ -30,10 +28,30 @@ import okhttp3.Call;
*/
public final class OkHttpDataSourceFactory extends BaseFactory {
@NonNull private final Call.Factory callFactory;
@Nullable private final String userAgent;
@Nullable private final TransferListener<? super DataSource> listener;
@Nullable private final CacheControl cacheControl;
private final Call.Factory callFactory;
private final @Nullable String userAgent;
private final @Nullable TransferListener listener;
private final @Nullable CacheControl cacheControl;
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory.
* @param userAgent An optional User-Agent string.
*/
public OkHttpDataSourceFactory(Call.Factory callFactory, @Nullable String userAgent) {
this(callFactory, userAgent, /* listener= */ null, /* cacheControl= */ null);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory.
* @param userAgent An optional User-Agent string.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
*/
public OkHttpDataSourceFactory(
Call.Factory callFactory, @Nullable String userAgent, @Nullable CacheControl cacheControl) {
this(callFactory, userAgent, /* listener= */ null, cacheControl);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
......@@ -41,9 +59,9 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
* @param userAgent An optional User-Agent string.
* @param listener An optional listener.
*/
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
@Nullable TransferListener<? super DataSource> listener) {
this(callFactory, userAgent, listener, null);
public OkHttpDataSourceFactory(
Call.Factory callFactory, @Nullable String userAgent, @Nullable TransferListener listener) {
this(callFactory, userAgent, listener, /* cacheControl= */ null);
}
/**
......@@ -53,8 +71,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
* @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
*/
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
@Nullable TransferListener<? super DataSource> listener,
public OkHttpDataSourceFactory(
Call.Factory callFactory,
@Nullable String userAgent,
@Nullable TransferListener listener,
@Nullable CacheControl cacheControl) {
this.callFactory = callFactory;
this.userAgent = userAgent;
......@@ -65,8 +85,16 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
@Override
protected OkHttpDataSource createDataSourceInternal(
HttpDataSource.RequestProperties defaultRequestProperties) {
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl,
defaultRequestProperties);
OkHttpDataSource dataSource =
new OkHttpDataSource(
callFactory,
userAgent,
/* contentTypePredicate= */ null,
cacheControl,
defaultRequestProperties);
if (listener != null) {
dataSource.addTransferListener(listener);
}
return dataSource;
}
}
......@@ -18,10 +18,16 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
sourceSets.main {
......@@ -32,6 +38,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
}
ext {
......
......@@ -26,6 +26,6 @@
<instrumentation
android:targetPackage="com.google.android.exoplayer2.ext.opus.test"
android:name="android.test.InstrumentationTestRunner"/>
android:name="androidx.test.runner.AndroidJUnitRunner"/>
</manifest>
......@@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2.ext.opus;
import static androidx.test.InstrumentationRegistry.getContext;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
......@@ -29,43 +32,40 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Playback tests using {@link LibopusAudioRenderer}.
*/
public class OpusPlaybackTest extends InstrumentationTestCase {
/** Playback tests using {@link LibopusAudioRenderer}. */
@RunWith(AndroidJUnit4.class)
public class OpusPlaybackTest {
private static final String BEAR_OPUS_URI = "asset:///bear-opus.webm";
@Override
protected void setUp() throws Exception {
super.setUp();
@Before
public void setUp() {
if (!OpusLibrary.isAvailable()) {
fail("Opus library not available.");
}
}
public void testBasicPlayback() throws ExoPlaybackException {
@Test
public void testBasicPlayback() throws Exception {
playUri(BEAR_OPUS_URI);
}
private void playUri(String uri) throws ExoPlaybackException {
TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri),
getInstrumentation().getContext());
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(Uri.parse(uri), getContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
fail(); // Should never happen.
}
thread.join();
if (testPlaybackRunnable.playbackException != null) {
throw testPlaybackRunnable.playbackException;
}
}
private static class TestPlaybackRunnable extends Player.DefaultEventListener
implements Runnable {
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
private final Context context;
private final Uri uri;
......
......@@ -30,8 +30,10 @@ import com.google.android.exoplayer2.util.MimeTypes;
*/
public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
/** The number of input and output buffers. */
private static final int NUM_BUFFERS = 16;
private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
/** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
private OpusDecoder decoder;
......@@ -88,8 +90,15 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws OpusDecoderException {
decoder = new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
format.initializationData, mediaCrypto);
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
decoder =
new OpusDecoder(
NUM_BUFFERS,
NUM_BUFFERS,
initialInputBufferSize,
format.initializationData,
mediaCrypto);
return decoder;
}
......
......@@ -18,6 +18,11 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 15
targetSdkVersion project.ext.targetSdkVersion
......
......@@ -19,6 +19,7 @@ import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TransferListener;
......@@ -26,40 +27,40 @@ import java.io.IOException;
import net.butterflytv.rtmp_client.RtmpClient;
import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException;
/**
* A Real-Time Messaging Protocol (RTMP) {@link DataSource}.
*/
public final class RtmpDataSource implements DataSource {
/** A Real-Time Messaging Protocol (RTMP) {@link DataSource}. */
public final class RtmpDataSource extends BaseDataSource {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp");
}
@Nullable private final TransferListener<? super RtmpDataSource> listener;
private RtmpClient rtmpClient;
private Uri uri;
public RtmpDataSource() {
this(null);
super(/* isNetwork= */ true);
}
/**
* @param listener An optional listener.
* @deprecated Use {@link #RtmpDataSource()} and {@link #addTransferListener(TransferListener)}.
*/
public RtmpDataSource(@Nullable TransferListener<? super RtmpDataSource> listener) {
this.listener = listener;
@Deprecated
public RtmpDataSource(@Nullable TransferListener listener) {
this();
if (listener != null) {
addTransferListener(listener);
}
}
@Override
public long open(DataSpec dataSpec) throws RtmpIOException {
transferInitializing(dataSpec);
rtmpClient = new RtmpClient();
rtmpClient.open(dataSpec.uri.toString(), false);
this.uri = dataSpec.uri;
if (listener != null) {
listener.onTransferStart(this, dataSpec);
}
transferStarted(dataSpec);
return C.LENGTH_UNSET;
}
......@@ -69,9 +70,7 @@ public final class RtmpDataSource implements DataSource {
if (bytesRead == -1) {
return C.RESULT_END_OF_INPUT;
}
if (listener != null) {
listener.onBytesTransferred(this, bytesRead);
}
bytesTransferred(bytesRead);
return bytesRead;
}
......@@ -79,9 +78,7 @@ public final class RtmpDataSource implements DataSource {
public void close() {
if (uri != null) {
uri = null;
if (listener != null) {
listener.onTransferEnd(this);
}
transferEnded();
}
if (rtmpClient != null) {
rtmpClient.close();
......
......@@ -25,23 +25,24 @@ import com.google.android.exoplayer2.upstream.TransferListener;
*/
public final class RtmpDataSourceFactory implements DataSource.Factory {
@Nullable
private final TransferListener<? super RtmpDataSource> listener;
private final @Nullable TransferListener listener;
public RtmpDataSourceFactory() {
this(null);
}
/**
* @param listener An optional listener.
*/
public RtmpDataSourceFactory(@Nullable TransferListener<? super RtmpDataSource> listener) {
/** @param listener An optional listener. */
public RtmpDataSourceFactory(@Nullable TransferListener listener) {
this.listener = listener;
}
@Override
public DataSource createDataSource() {
return new RtmpDataSource(listener);
RtmpDataSource dataSource = new RtmpDataSource();
if (listener != null) {
dataSource.addTransferListener(listener);
}
return dataSource;
}
}
......@@ -49,7 +49,7 @@ git clone https://chromium.googlesource.com/libyuv/libyuv libyuv
```
cd "${VP9_EXT_PATH}/jni/libvpx" && \
git checkout tags/v1.6.1 -b v1.6.1 && \
git checkout tags/v1.7.0 -b v1.7.0 && \
cd "${VP9_EXT_PATH}/jni/libyuv" && \
git checkout 996a2bbd
```
......
......@@ -18,10 +18,16 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
sourceSets.main {
......@@ -33,6 +39,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
}
......
......@@ -26,6 +26,6 @@
<instrumentation
android:targetPackage="com.google.android.exoplayer2.ext.vp9.test"
android:name="android.test.InstrumentationTestRunner"/>
android:name="androidx.test.runner.AndroidJUnitRunner"/>
</manifest>
......@@ -15,13 +15,14 @@
*/
package com.google.android.exoplayer2.ext.vp9;
import static androidx.test.InstrumentationRegistry.getContext;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import android.util.Log;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
......@@ -32,11 +33,14 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Log;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Playback tests using {@link LibvpxVideoRenderer}.
*/
public class VpxPlaybackTest extends InstrumentationTestCase {
/** Playback tests using {@link LibvpxVideoRenderer}. */
@RunWith(AndroidJUnit4.class)
public class VpxPlaybackTest {
private static final String BEAR_URI = "asset:///bear-vp9.webm";
private static final String BEAR_ODD_DIMENSIONS_URI = "asset:///bear-vp9-odd-dimensions.webm";
......@@ -45,23 +49,25 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
private static final String TAG = "VpxPlaybackTest";
@Override
protected void setUp() throws Exception {
super.setUp();
@Before
public void setUp() {
if (!VpxLibrary.isAvailable()) {
fail("Vpx library not available.");
}
}
public void testBasicPlayback() throws ExoPlaybackException {
@Test
public void testBasicPlayback() throws Exception {
playUri(BEAR_URI);
}
public void testOddDimensionsPlayback() throws ExoPlaybackException {
@Test
public void testOddDimensionsPlayback() throws Exception {
playUri(BEAR_ODD_DIMENSIONS_URI);
}
public void test10BitProfile2Playback() throws ExoPlaybackException {
@Test
public void test10BitProfile2Playback() throws Exception {
if (VpxLibrary.isHighBitDepthSupported()) {
Log.d(TAG, "High Bit Depth supported.");
playUri(ROADTRIP_10BIT_URI);
......@@ -70,6 +76,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
Log.d(TAG, "High Bit Depth not supported.");
}
@Test
public void testInvalidBitstream() {
try {
playUri(INVALID_BITSTREAM_URI);
......@@ -80,23 +87,18 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
}
}
private void playUri(String uri) throws ExoPlaybackException {
TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri),
getInstrumentation().getContext());
private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(Uri.parse(uri), getContext());
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
fail(); // Should never happen.
}
thread.join();
if (testPlaybackRunnable.playbackException != null) {
throw testPlaybackRunnable.playbackException;
}
}
private static class TestPlaybackRunnable extends Player.DefaultEventListener
implements Runnable {
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
private final Context context;
private final Uri uri;
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.vp9;
import android.view.Surface;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
......@@ -31,6 +32,7 @@ import java.nio.ByteBuffer;
public static final int OUTPUT_MODE_NONE = -1;
public static final int OUTPUT_MODE_YUV = 0;
public static final int OUTPUT_MODE_RGB = 1;
public static final int OUTPUT_MODE_SURFACE_YUV = 2;
private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = 1;
......@@ -50,10 +52,17 @@ import java.nio.ByteBuffer;
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
* @param enableSurfaceYuvOutputMode Whether OUTPUT_MODE_SURFACE_YUV is allowed.
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
*/
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter) throws VpxDecoderException {
public VpxDecoder(
int numInputBuffers,
int numOutputBuffers,
int initialInputBufferSize,
ExoMediaCrypto exoMediaCrypto,
boolean disableLoopFilter,
boolean enableSurfaceYuvOutputMode)
throws VpxDecoderException {
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries.");
......@@ -62,7 +71,7 @@ import java.nio.ByteBuffer;
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
}
vpxDecContext = vpxInit(disableLoopFilter);
vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode);
if (vpxDecContext == 0) {
throw new VpxDecoderException("Failed to initialize decoder");
}
......@@ -96,6 +105,11 @@ import java.nio.ByteBuffer;
@Override
protected void releaseOutputBuffer(VpxOutputBuffer buffer) {
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
// require a call to vpxReleaseFrame.
if (outputMode == OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
vpxReleaseFrame(vpxDecContext, buffer);
}
super.releaseOutputBuffer(buffer);
}
......@@ -145,13 +159,36 @@ import java.nio.ByteBuffer;
vpxClose(vpxDecContext);
}
private native long vpxInit(boolean disableLoopFilter);
/** Renders the outputBuffer to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. */
public void renderToSurface(VpxOutputBuffer outputBuffer, Surface surface)
throws VpxDecoderException {
int getFrameResult = vpxRenderFrame(vpxDecContext, surface, outputBuffer);
if (getFrameResult == -1) {
throw new VpxDecoderException("Buffer render failed.");
}
}
private native long vpxInit(boolean disableLoopFilter, boolean enableSurfaceYuvOutputMode);
private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length);
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
/**
* Renders the frame to the surface. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called
* if {@link #vpxInit} was called with {@code enableBufferManager = true}.
*/
private native int vpxRenderFrame(long context, Surface surface, VpxOutputBuffer outputBuffer);
/**
* Releases the frame. Used with OUTPUT_MODE_SURFACE_YUV only. Must only be called if {@link
* #vpxInit} was called with {@code enableBufferManager = true}.
*/
private native int vpxReleaseFrame(long context, VpxOutputBuffer outputBuffer);
private native int vpxGetErrorCode(long context);
private native String vpxGetErrorMessage(long context);
......
......@@ -30,6 +30,8 @@ import java.nio.ByteBuffer;
public static final int COLORSPACE_BT2020 = 3;
private final VpxDecoder owner;
/** Decoder private data. */
public int decoderPrivate;
public int mode;
/**
......
......@@ -35,7 +35,7 @@ LOCAL_MODULE := libvpxJNI
LOCAL_ARM_MODE := arm
LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := vpx_jni.cc
LOCAL_LDLIBS := -llog -lz -lm
LOCAL_LDLIBS := -llog -lz -lm -landroid
LOCAL_SHARED_LIBRARIES := libvpx
LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures
include $(BUILD_SHARED_LIBRARY)
......
......@@ -39,7 +39,7 @@ class CombinedJavadocPlugin implements Plugin<Project> {
libraryModules.each { libraryModule ->
libraryModule.android.libraryVariants.all { variant ->
def name = variant.buildType.name
if (name.equals("release")) {
if (name == "release") {
classpath +=
libraryModule.project.files(
variant.javaCompile.classpath.files,
......@@ -63,7 +63,7 @@ class CombinedJavadocPlugin implements Plugin<Project> {
}
// Returns Android library modules that declare a generateJavadoc task.
private Set<Project> getLibraryModules(Project project) {
private static Set<Project> getLibraryModules(Project project) {
project.subprojects.findAll {
it.plugins.findPlugin("com.android.library") &&
it.tasks.findByName("generateJavadoc")
......
......@@ -18,12 +18,17 @@ android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
......@@ -34,11 +39,11 @@ android {
// Workaround to prevent circular dependency on project :testutils.
sourceSets {
androidTest {
java.srcDirs += "../../testutils/src/main/java/"
java.srcDirs += '../../testutils/src/main/java/'
}
test {
java.srcDirs += "../../testutils/src/main/java/"
java.srcDirs += "../../testutils_robolectric/src/main/java/"
java.srcDirs += '../../testutils/src/main/java/'
java.srcDirs += '../../testutils_robolectric/src/main/java/'
}
}
......@@ -54,12 +59,14 @@ android {
dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion
androidTestImplementation 'com.android.support.test:runner:' + testRunnerVersion
androidTestUtil 'com.android.support.test:orchestrator:' + testRunnerVersion
androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
......
......@@ -30,5 +30,10 @@
<init>();
}
# Constructors accessed via reflection in DownloadAction
-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloadAction
-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloadAction
-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadAction
# Don't warn about checkerframework
-dontwarn org.checkerframework.**
......@@ -29,6 +29,6 @@
<instrumentation
android:targetPackage="com.google.android.exoplayer2.core.test"
android:name="android.test.InstrumentationTestRunner"/>
android:name="androidx.test.runner.AndroidJUnitRunner"/>
</manifest>
The file could not be displayed because it is too large.
No preview for this file type
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