Merge pull request #3882 from google/dev-v2-r2.7.0

r2.7.0
This commit is contained in:
ojw28 2018-02-22 11:48:01 +00:00 committed by GitHub
commit eb9c67fbc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
659 changed files with 28761 additions and 12888 deletions

View file

@ -1,5 +1,130 @@
# Release notes #
### 2.7.0 ###
* Player interface:
* Add optional parameter to `stop` to reset the player when stopping.
* Add a reason to `EventListener.onTimelineChanged` to distinguish between
initial preparation, reset and dynamic updates.
* Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons
reported in `Eventlistener.onPositionDiscontinuity` to distinguish
transitions to and from ads within one period from transitions between
periods.
* Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow
more customization of the message. Now supports setting a message delivery
playback position and/or a delivery handler
([#2189](https://github.com/google/ExoPlayer/issues/2189)).
* Add `Player.VideoComponent`, `Player.TextComponent` and
`Player.MetadataComponent` interfaces that define optional video, text and
metadata output functionality. New `getVideoComponent`, `getTextComponent`
and `getMetadataComponent` methods provide access to this functionality.
* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are
performed. The `SeekParameters` class contains defaults for exact seeking and
seeking to the closest sync points before, either side or after specified seek
positions. `SeekParameters` are not currently supported when playing HLS
streams.
* DefaultTrackSelector:
* Replace `DefaultTrackSelector.Parameters` copy methods with a builder.
* Support disabling of individual text track selection flags.
* Buffering:
* Allow a back-buffer of media to be retained behind the current playback
position, for fast backward seeking. The back-buffer can be configured by
custom `LoadControl` implementations.
* Add ability for `SequenceableLoader` to re-evaluate its buffer and discard
buffered media so that it can be re-buffered in a different quality.
* Allow more flexible loading strategy when playing media containing multiple
sub-streams, by allowing injection of custom `CompositeSequenceableLoader`
factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`,
`SsMediaSource.Factory`, and `MergingMediaSource`.
* Play out existing buffer before retrying for progressive live streams
([#1606](https://github.com/google/ExoPlayer/issues/1606)).
* UI components:
* Generalized player and control views to allow them to bind with any
`Player`, and renamed them to `PlayerView` and `PlayerControlView`
respectively.
* Made `PlayerView` automatically apply video rotation when configured to use
`TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)).
* Made `PlayerView` play button behave correctly when the player is ended
([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a
`PlaybackPreparer` when the player is idle.
* DRM: Optimistically attempt playback of DRM protected content that does not
declare scheme specific init data in the manifest. If playback of clear
samples without keys is allowed, delay DRM session error propagation until
keys are actually needed
([#3630](https://github.com/google/ExoPlayer/issues/3630)).
* DASH:
* Support in-band Emsg events targeting the player with scheme id
"urn:mpeg:dash:event:2012" and scheme values "1", "2" and "3".
* Support EventStream elements in DASH manifests.
* HLS:
* Add opt-in support for chunkless preparation in HLS. This allows an
HLS source to finish preparation without downloading any chunks, which can
significantly reduce initial buffering time
([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details
can be found
[here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6).
* Fail if unable to sync with the Transport Stream, rather than entering
stuck in an indefinite buffering state.
* Fix mime type propagation
([#3653](https://github.com/google/ExoPlayer/issues/3653)).
* Fix ID3 context reuse across segment format changes
([#3622](https://github.com/google/ExoPlayer/issues/3622)).
* Use long for media sequence numbers
([#3747](https://github.com/google/ExoPlayer/issues/3747))
* Add initial support for the EXT-X-GAP tag.
* Audio:
* Support TrueHD passthrough for rechunked samples in Matroska files
([#2147](https://github.com/google/ExoPlayer/issues/2147)).
* Support resampling 24-bit and 32-bit integer to 32-bit float for high
resolution output in `DefaultAudioSink`
([#3635](https://github.com/google/ExoPlayer/pull/3635)).
* Captions:
* Basic support for PGS subtitles
([#3008](https://github.com/google/ExoPlayer/issues/3008)).
* Fix handling of CEA-608 captions where multiple buffers have the same
presentation timestamp
([#3782](https://github.com/google/ExoPlayer/issues/3782)).
* Caching:
* Fix cache corruption issue
([#3762](https://github.com/google/ExoPlayer/issues/3762)).
* Implement periodic check in `CacheDataSource` to see whether it's possible
to switch to reading/writing the cache having initially bypassed it.
* IMA extension:
* Fix the player getting stuck when an ad group fails to load
([#3584](https://github.com/google/ExoPlayer/issues/3584)).
* Work around loadAd not being called beore the LOADED AdEvent arrives
([#3552](https://github.com/google/ExoPlayer/issues/3552)).
* Handle asset mismatch errors
([#3801](https://github.com/google/ExoPlayer/issues/3801)).
* Add support for playing non-Extractor content MediaSources in
the IMA demo app
([#3676](https://github.com/google/ExoPlayer/issues/3676)).
* Fix handling of ad tags where ad groups are out of order
([#3716](https://github.com/google/ExoPlayer/issues/3716)).
* Fix handling of ad tags with only preroll/postroll ad groups
([#3715](https://github.com/google/ExoPlayer/issues/3715)).
* Propagate ad media preparation errors to IMA so that the ads can be
skipped.
* Handle exceptions in IMA callbacks so that can be logged less verbosely.
* New Cast extension. Simplifies toggling between local and Cast playbacks.
* `EventLogger` moved from the demo app into the core library.
* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C,
Lenovo K4 Note and Sony Xperia E5.
([#3724](https://github.com/google/ExoPlayer/issues/3724),
[#3835](https://github.com/google/ExoPlayer/issues/3835)).
* Fix potential NPE when removing media sources from a
DynamicConcatenatingMediaSource
([#3796](https://github.com/google/ExoPlayer/issues/3796)).
* Check `sys.display-size` on Philips ATVs
([#3807](https://github.com/google/ExoPlayer/issues/3807)).
* Release `Extractor`s on the loading thread to avoid potentially leaking
resources when the playback thread has quit by the time the loading task has
completed.
* ID3: Better handle malformed ID3 data
([#3792](https://github.com/google/ExoPlayer/issues/3792).
* Support 14-bit mode and little endianness in DTS PES packets
([#3340](https://github.com/google/ExoPlayer/issues/3340)).
### 2.6.1 ###
* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`,
@ -41,6 +166,8 @@
([#3188](https://github.com/google/ExoPlayer/issues/3188)).
* CEA-608: Fix handling of row count changes in roll-up mode
([#3513](https://github.com/google/ExoPlayer/issues/3513)).
* Prevent period transitions when seeking to the end of a period when paused
([#2439](https://github.com/google/ExoPlayer/issues/2439)).
### 2.6.0 ###

View file

@ -26,9 +26,9 @@ project.ext {
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
junitVersion = '4.12'
truthVersion = '0.35'
robolectricVersion = '3.4.2'
releaseVersion = '2.6.1'
truthVersion = '0.39'
robolectricVersion = '3.7.1'
releaseVersion = '2.7.0'
modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix

View file

@ -28,6 +28,7 @@ include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr'
include modulePrefix + 'extension-ima'
include modulePrefix + 'extension-cast'
include modulePrefix + 'extension-mediasession'
include modulePrefix + 'extension-okhttp'
include modulePrefix + 'extension-opus'
@ -46,6 +47,7 @@ project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'exten
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')

4
demos/cast/README.md Normal file
View file

@ -0,0 +1,4 @@
# Cast demo application #
This folder contains a demo application that showcases ExoPlayer integration
with Google Cast.

52
demos/cast/build.gradle Normal file
View file

@ -0,0 +1,52 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 16
targetSdkVersion project.ext.targetSdkVersion
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
debug {
jniDebuggable = true
}
}
lintOptions {
// The demo app does not have translations.
disable 'MissingTranslation'
}
}
dependencies {
compile project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-dash')
compile project(modulePrefix + 'library-hls')
compile project(modulePrefix + 'library-smoothstreaming')
compile project(modulePrefix + 'library-ui')
compile project(modulePrefix + 'extension-cast')
compile 'com.android.support:recyclerview-v7:' + supportLibraryVersion
}

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.castdemo"
android:versionCode="2700"
android:versionName="2.7.0">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider" />
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop" android:label="@string/application_name"
android:theme="@style/Theme.AppCompat">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,92 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.castdemo;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Utility methods and constants for the Cast demo application.
*/
/* package */ final class DemoUtil {
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/**
* The list of samples available in the cast demo app.
*/
public static final List<Sample> SAMPLES;
/**
* Represents a media sample.
*/
public static final class Sample {
/**
* The uri from which the media sample is obtained.
*/
public final String uri;
/**
* A descriptive name for the sample.
*/
public final String name;
/**
* The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
*/
public final String mimeType;
/**
* @param uri See {@link #uri}.
* @param name See {@link #name}.
* @param mimeType See {@link #mimeType}.
*/
public Sample(String uri, String name, String mimeType) {
this.uri = uri;
this.name = name;
this.mimeType = mimeType;
}
@Override
public String toString() {
return name;
}
}
static {
// App samples.
ArrayList<Sample> samples = new ArrayList<>();
samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
"DASH (clear,MP4,H264)", MIME_TYPE_DASH));
samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
+ "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS));
samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)",
MIME_TYPE_VIDEO_MP4));
SAMPLES = Collections.unmodifiableList(samples);
}
private DemoUtil() {}
}

View file

@ -0,0 +1,261 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.graphics.ColorUtils;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext;
/**
* An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
*/
public class MainActivity extends AppCompatActivity implements OnClickListener,
PlayerManager.QueuePositionListener {
private PlayerView localPlayerView;
private PlayerControlView castControlView;
private PlayerManager playerManager;
private RecyclerView mediaQueueList;
private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext;
// Activity lifecycle methods.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Getting the cast context later than onStart can cause device discovery not to take place.
castContext = CastContext.getSharedInstance(this);
setContentView(R.layout.main_activity);
localPlayerView = findViewById(R.id.local_player_view);
localPlayerView.requestFocus();
castControlView = findViewById(R.id.cast_control_view);
mediaQueueList = findViewById(R.id.sample_list);
ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback());
helper.attachToRecyclerView(mediaQueueList);
mediaQueueList.setLayoutManager(new LinearLayoutManager(this));
mediaQueueList.setHasFixedSize(true);
mediaQueueListAdapter = new MediaQueueListAdapter();
findViewById(R.id.add_sample_button).setOnClickListener(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.menu, menu);
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item);
return true;
}
@Override
public void onResume() {
super.onResume();
playerManager =
PlayerManager.createPlayerManager(
/* queuePositionListener= */ this,
localPlayerView,
castControlView,
/* context= */ this,
castContext);
mediaQueueList.setAdapter(mediaQueueListAdapter);
}
@Override
public void onPause() {
super.onPause();
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
mediaQueueList.setAdapter(null);
playerManager.release();
}
// Activity input.
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// If the event was not handled then see if the player view can handle it.
return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event);
}
@Override
public void onClick(View view) {
new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title)
.setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create()
.show();
}
// PlayerManager.QueuePositionListener implementation.
@Override
public void onQueuePositionChanged(int previousIndex, int newIndex) {
if (previousIndex != C.INDEX_UNSET) {
mediaQueueListAdapter.notifyItemChanged(previousIndex);
}
if (newIndex != C.INDEX_UNSET) {
mediaQueueListAdapter.notifyItemChanged(newIndex);
}
}
// Internal methods.
private View buildSampleListView() {
View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);
ListView sampleList = dialogList.findViewById(R.id.sample_list);
sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener(
new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
playerManager.addItem(DemoUtil.SAMPLES.get(position));
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
}
});
return dialogList;
}
// Internal classes.
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
public final TextView textView;
public QueueItemViewHolder(TextView textView) {
super(textView);
this.textView = textView;
textView.setOnClickListener(this);
}
@Override
public void onClick(View v) {
playerManager.selectQueueItem(getAdapterPosition());
}
}
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
@Override
public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
TextView v = (TextView) LayoutInflater.from(parent.getContext())
.inflate(android.R.layout.simple_list_item_1, parent, false);
return new QueueItemViewHolder(v);
}
@Override
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
TextView view = holder.textView;
view.setText(playerManager.getItem(position).name);
// TODO: Solve coloring using the theme's ColorStateList.
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(),
position == playerManager.getCurrentItemIndex() ? 255 : 100));
}
@Override
public int getItemCount() {
return playerManager.getMediaQueueSize();
}
}
private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback {
private int draggingFromPosition;
private int draggingToPosition;
public RecyclerViewCallback() {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END);
draggingFromPosition = C.INDEX_UNSET;
draggingToPosition = C.INDEX_UNSET;
}
@Override
public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin,
RecyclerView.ViewHolder target) {
int fromPosition = origin.getAdapterPosition();
int toPosition = target.getAdapterPosition();
if (draggingFromPosition == C.INDEX_UNSET) {
// A drag has started, but changes to the media queue will be reflected in clearView().
draggingFromPosition = fromPosition;
}
draggingToPosition = toPosition;
mediaQueueListAdapter.notifyItemMoved(fromPosition, toPosition);
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
if (playerManager.removeItem(position)) {
mediaQueueListAdapter.notifyItemRemoved(position);
}
}
@Override
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (draggingFromPosition != C.INDEX_UNSET) {
// A drag has ended. We reflect the media queue change in the player.
if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) {
// The move failed. The entire sequence of onMove calls since the drag started needs to be
// invalidated.
mediaQueueListAdapter.notifyDataSetChanged();
}
}
draggingFromPosition = C.INDEX_UNSET;
draggingToPosition = C.INDEX_UNSET;
}
}
private static final class SampleListAdapter extends ArrayAdapter<Sample> {
public SampleListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
}
}
}

View file

@ -0,0 +1,425 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.net.Uri;
import android.view.KeyEvent;
import android.view.View;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DefaultEventListener;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
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;
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 {
/**
* Listener for changes in the media queue playback position.
*/
public interface QueuePositionListener {
/**
* Called when the currently played item of the media queue changes.
*/
void onQueuePositionChanged(int previousIndex, int newIndex);
}
private static final String USER_AGENT = "ExoCastDemoPlayer";
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER);
private final PlayerView localPlayerView;
private final PlayerControlView castControlView;
private final SimpleExoPlayer exoPlayer;
private final CastPlayer castPlayer;
private final ArrayList<DemoUtil.Sample> mediaQueue;
private final QueuePositionListener queuePositionListener;
private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource;
private boolean castMediaQueueCreationPending;
private int currentItemIndex;
private Player currentPlayer;
/**
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes.
* @param localPlayerView The {@link PlayerView} for local playback.
* @param castControlView The {@link PlayerControlView} to control remote playback.
* @param context A {@link Context}.
* @param castContext The {@link CastContext}.
*/
public static PlayerManager createPlayerManager(
QueuePositionListener queuePositionListener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
PlayerManager playerManager =
new PlayerManager(
queuePositionListener, localPlayerView, castControlView, context, castContext);
playerManager.init();
return playerManager;
}
private PlayerManager(
QueuePositionListener queuePositionListener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
this.queuePositionListener = queuePositionListener;
this.localPlayerView = localPlayerView;
this.castControlView = castControlView;
mediaQueue = new ArrayList<>();
currentItemIndex = C.INDEX_UNSET;
DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER);
RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null);
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer);
castPlayer = new CastPlayer(castContext);
castPlayer.addListener(this);
castPlayer.setSessionAvailabilityListener(this);
castControlView.setPlayer(castPlayer);
}
// Queue manipulation methods.
/**
* Plays a specified queue item in the current player.
*
* @param itemIndex The index of the item to play.
*/
public void selectQueueItem(int itemIndex) {
setCurrentItem(itemIndex, C.TIME_UNSET, true);
}
/**
* Returns the index of the currently played item.
*/
public int getCurrentItemIndex() {
return currentItemIndex;
}
/**
* Appends {@code sample} to the media queue.
*
* @param sample The {@link Sample} to append.
*/
public void addItem(Sample sample) {
mediaQueue.add(sample);
if (currentPlayer == exoPlayer) {
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample));
} else {
castPlayer.addItems(buildMediaQueueItem(sample));
}
}
/**
* Returns the size of the media queue.
*/
public int getMediaQueueSize() {
return mediaQueue.size();
}
/**
* Returns the item at the given index in the media queue.
*
* @param position The index of the item.
* @return The item at the given index in the media queue.
*/
public Sample getItem(int position) {
return mediaQueue.get(position);
}
/**
* Removes the item at the given index from the media queue.
*
* @param itemIndex The index of the item to remove.
* @return Whether the removal was successful.
*/
public boolean removeItem(int itemIndex) {
if (currentPlayer == exoPlayer) {
dynamicConcatenatingMediaSource.removeMediaSource(itemIndex);
} else {
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
if (castTimeline.getPeriodCount() <= itemIndex) {
return false;
}
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
}
}
mediaQueue.remove(itemIndex);
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
} else if (itemIndex < currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
}
return true;
}
/**
* Moves an item within the queue.
*
* @param fromIndex The index of the item to move.
* @param toIndex The target index of the item in the queue.
* @return Whether the item move was successful.
*/
public boolean moveItem(int fromIndex, int toIndex) {
// Player update.
if (currentPlayer == exoPlayer) {
dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
} else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
int periodCount = castTimeline.getPeriodCount();
if (periodCount <= fromIndex || periodCount <= toIndex) {
return false;
}
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
castPlayer.moveItem(elementId, toIndex);
}
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
// Index update.
if (fromIndex == currentItemIndex) {
maybeSetCurrentItemAndNotify(toIndex);
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
}
return true;
}
// Miscellaneous methods.
/**
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
*
* @param event The {@link KeyEvent}.
* @return Whether the event was handled by the target view.
*/
public boolean dispatchKeyEvent(KeyEvent event) {
if (currentPlayer == exoPlayer) {
return localPlayerView.dispatchKeyEvent(event);
} else /* currentPlayer == castPlayer */ {
return castControlView.dispatchKeyEvent(event);
}
}
/**
* Releases the manager and the players that it holds.
*/
public void release() {
currentItemIndex = C.INDEX_UNSET;
mediaQueue.clear();
castPlayer.setSessionAvailabilityListener(null);
castPlayer.release();
localPlayerView.setPlayer(null);
exoPlayer.release();
}
// Player.EventListener implementation.
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
updateCurrentItemIndex();
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
updateCurrentItemIndex();
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
updateCurrentItemIndex();
}
// CastPlayer.SessionAvailabilityListener implementation.
@Override
public void onCastSessionAvailable() {
setCurrentPlayer(castPlayer);
}
@Override
public void onCastSessionUnavailable() {
setCurrentPlayer(exoPlayer);
}
// Internal methods.
private void init() {
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
}
private void updateCurrentItemIndex() {
int playbackState = currentPlayer.getPlaybackState();
maybeSetCurrentItemAndNotify(
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET);
}
private void setCurrentPlayer(Player currentPlayer) {
if (this.currentPlayer == currentPlayer) {
return;
}
// View management.
if (currentPlayer == exoPlayer) {
localPlayerView.setVisibility(View.VISIBLE);
castControlView.hide();
} else /* currentPlayer == castPlayer */ {
localPlayerView.setVisibility(View.GONE);
castControlView.show();
}
// Player state management.
long playbackPositionMs = C.TIME_UNSET;
int windowIndex = C.INDEX_UNSET;
boolean playWhenReady = false;
if (this.currentPlayer != null) {
int playbackState = this.currentPlayer.getPlaybackState();
if (playbackState != Player.STATE_ENDED) {
playbackPositionMs = this.currentPlayer.getCurrentPosition();
playWhenReady = this.currentPlayer.getPlayWhenReady();
windowIndex = this.currentPlayer.getCurrentWindowIndex();
if (windowIndex != currentItemIndex) {
playbackPositionMs = C.TIME_UNSET;
windowIndex = currentItemIndex;
}
}
this.currentPlayer.stop(true);
} else {
// This is the initial setup. No need to save any state.
}
this.currentPlayer = currentPlayer;
// Media queue management.
castMediaQueueCreationPending = currentPlayer == castPlayer;
if (currentPlayer == exoPlayer) {
dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource();
for (int i = 0; i < mediaQueue.size(); i++) {
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i)));
}
exoPlayer.prepare(dynamicConcatenatingMediaSource);
}
// Playback transition.
if (windowIndex != C.INDEX_UNSET) {
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
}
}
/**
* Starts playback of the item at the given position.
*
* @param itemIndex The index of the item to play.
* @param positionMs The position at which playback should start.
* @param playWhenReady Whether the player should proceed when ready to do so.
*/
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
maybeSetCurrentItemAndNotify(itemIndex);
if (castMediaQueueCreationPending) {
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
for (int i = 0; i < items.length; i++) {
items[i] = buildMediaQueueItem(mediaQueue.get(i));
}
castMediaQueueCreationPending = false;
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
} else {
currentPlayer.seekTo(itemIndex, positionMs);
currentPlayer.setPlayWhenReady(playWhenReady);
}
}
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
if (this.currentItemIndex != currentItemIndex) {
int oldIndex = this.currentItemIndex;
this.currentItemIndex = currentItemIndex;
queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex);
}
}
private static MediaSource buildMediaSource(DemoUtil.Sample sample) {
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);
case DemoUtil.MIME_TYPE_DASH:
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(DATA_SOURCE_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:
return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
default: {
throw new IllegalStateException("Unsupported type: " + sample.mimeType);
}
}
}
private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) {
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name);
MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType)
.setMetadata(movieMetadata).build();
return new MediaQueueItem.Builder(mediaInfo).build();
}
}

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector android:alpha="0.8" android:height="24dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
</vector>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="12"
app:repeat_toggle_modes="all|one"/>
<RelativeLayout android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="12">
<android.support.v7.widget.RecyclerView android:id="@+id/sample_list"
android:choiceMode="singleChoice"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:fadeScrollbars="false"/>
<ImageButton android:id="@+id/add_sample_button"
android:background="@drawable/ic_add_circle_white_24dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:padding="30dp"/>
</RelativeLayout>
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:visibility="gone"
app:repeat_toggle_modes="all|one"
app:show_timeout="-1"/>
</LinearLayout>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<ListView android:id="@+id/sample_list"
android:layout_width="match_parent"
android:layout_height="250dp"
android:fadeScrollbars="false"/>
</LinearLayout>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title"
app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
app:showAsAction="always" />
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="application_name">Exo Cast Demo</string>
<string name="media_route_menu_title">Cast</string>
<string name="sample_list_dialog_title">Add samples</string>
</resources>

View file

@ -45,5 +45,6 @@ dependencies {
compile project(modulePrefix + 'library-ui')
compile project(modulePrefix + 'library-dash')
compile project(modulePrefix + 'library-hls')
compile project(modulePrefix + 'library-smoothstreaming')
compile project(modulePrefix + 'extension-ima')
}

View file

@ -15,8 +15,8 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.imademo"
android:versionCode="2601"
android:versionName="2.6.1">
android:versionCode="2700"
android:versionName="2.7.0">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="27"/>

View file

@ -18,7 +18,7 @@ package com.google.android.exoplayer2.imademo;
import android.app.Activity;
import android.os.Bundle;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.ui.PlayerView;
/**
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
@ -26,7 +26,7 @@ import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
*/
public final class MainActivity extends Activity {
private SimpleExoPlayerView playerView;
private PlayerView playerView;
private PlayerManager player;
@Override

View file

@ -32,11 +32,13 @@ 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.SimpleExoPlayerView;
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;
@ -66,7 +68,7 @@ import com.google.android.exoplayer2.util.Util;
new DefaultBandwidthMeter());
}
public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) {
public void init(Context context, PlayerView playerView) {
// Create a default track selector.
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory =
@ -77,17 +79,12 @@ import com.google.android.exoplayer2.util.Util;
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
// Bind the player to the view.
simpleExoPlayerView.setPlayer(player);
// Produces DataSource instances through which media data is loaded.
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
Util.getUserAgent(context, context.getString(R.string.application_name)));
playerView.setPlayer(player);
// This is the MediaSource representing the content media (i.e. not the ad).
String contentUrl = context.getString(R.string.content_url);
MediaSource contentMediaSource =
new ExtractorMediaSource.Factory(dataSourceFactory)
.createMediaSource(Uri.parse(contentUrl));
buildMediaSource(Uri.parse(contentUrl), /* handler= */ null, /* listener= */ null);
// Compose the content media source into a new AdsMediaSource with both ads and content.
MediaSource mediaSourceWithAds =
@ -95,7 +92,7 @@ import com.google.android.exoplayer2.util.Util;
contentMediaSource,
/* adMediaSourceFactory= */ this,
adsLoader,
simpleExoPlayerView.getOverlayFrameLayout(),
playerView.getOverlayFrameLayout(),
/* eventHandler= */ null,
/* eventListener= */ null);
@ -126,6 +123,19 @@ import com.google.android.exoplayer2.util.Util;
@Override
public MediaSource createMediaSource(
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
return buildMediaSource(uri, handler, listener);
}
@Override
public int[] getSupportedTypes() {
// IMA does not support Smooth Streaming ads.
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
}
// Internal methods.
private MediaSource buildMediaSource(
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
@ContentType int type = Util.inferContentType(uri);
switch (type) {
case C.TYPE_DASH:
@ -133,20 +143,19 @@ import com.google.android.exoplayer2.util.Util;
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
manifestDataSourceFactory)
.createMediaSource(uri, handler, listener);
case C.TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory)
.createMediaSource(uri, handler, listener);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri, handler, listener);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri, handler, listener);
case C.TYPE_SS:
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
@Override
public int[] getSupportedTypes() {
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
}
}

View file

@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
<com.google.android.exoplayer2.ui.PlayerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/player_view"
android:layout_width="match_parent"

View file

@ -27,7 +27,10 @@ android {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
proguardFiles = [
"proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt')
]
}
debug {
jniDebuggable = true

View file

@ -0,0 +1,7 @@
# Proguard rules specific to the main demo app.
# Constructor accessed via reflection in PlayerActivity
-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
<init>(android.content.Context, android.net.Uri);
}

View file

@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2601"
android:versionName="2.6.1">
android:versionCode="2700"
android:versionName="2.7.0">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

View file

@ -540,7 +540,7 @@
{
"name": "VMAP pre-, mid- and post-rolls, single ads",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator="
},
{
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad",
@ -566,6 +566,16 @@
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
},
{
"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"
},
{
"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"
}
]
}

View file

@ -17,10 +17,10 @@ package com.google.android.exoplayer2.demo;
import android.app.Application;
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.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Util;
/**
@ -36,13 +36,15 @@ public class DemoApplication extends Application {
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
}
public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultDataSourceFactory(this, bandwidthMeter,
buildHttpDataSourceFactory(bandwidthMeter));
/** Returns a {@link DataSource.Factory}. */
public DataSource.Factory buildDataSourceFactory(TransferListener<? super DataSource> listener) {
return new DefaultDataSourceFactory(this, listener, buildHttpDataSourceFactory(listener));
}
public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
/** Returns a {@link HttpDataSource.Factory}. */
public HttpDataSource.Factory buildHttpDataSourceFactory(
TransferListener<? super DataSource> listener) {
return new DefaultHttpDataSourceFactory(userAgent, listener);
}
public boolean useExtensionRenderers() {

View file

@ -16,44 +16,15 @@
package com.google.android.exoplayer2.demo;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.Locale;
import java.util.UUID;
/**
* Utility methods for demo application.
*/
/* package */ final class DemoUtil {
/**
* Derives a DRM {@link UUID} from {@code drmScheme}.
*
* @param drmScheme A protection scheme UUID string; or {@code "widevine"}, {@code "playready"} or
* {@code "clearkey"}.
* @return The derived {@link UUID}.
* @throws UnsupportedDrmException If no {@link UUID} could be derived from {@code drmScheme}.
*/
public static UUID getDrmUuid(String drmScheme) throws UnsupportedDrmException {
switch (Util.toLowerInvariant(drmScheme)) {
case "widevine":
return C.WIDEVINE_UUID;
case "playready":
return C.PLAYREADY_UUID;
case "clearkey":
return C.CLEARKEY_UUID;
default:
try {
return UUID.fromString(drmScheme);
} catch (RuntimeException e) {
throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME);
}
}
}
/**
* Builds a track name for display.
*

View file

@ -16,7 +16,6 @@
package com.google.android.exoplayer2.demo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@ -39,6 +38,7 @@ import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
@ -68,22 +68,22 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.PlaybackControlView;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.Util;
import java.lang.reflect.Constructor;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.UUID;
/**
* An activity that plays media using {@link SimpleExoPlayer}.
*/
public class PlayerActivity extends Activity implements OnClickListener,
PlaybackControlView.VisibilityListener {
/** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends Activity
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
public static final String DRM_LICENSE_URL = "drm_license_url";
@ -112,10 +112,9 @@ public class PlayerActivity extends Activity implements OnClickListener,
private Handler mainHandler;
private EventLogger eventLogger;
private SimpleExoPlayerView simpleExoPlayerView;
private PlayerView playerView;
private LinearLayout debugRootView;
private TextView debugTextView;
private Button retryButton;
private DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player;
@ -153,12 +152,10 @@ public class PlayerActivity extends Activity implements OnClickListener,
rootView.setOnClickListener(this);
debugRootView = findViewById(R.id.controls_root);
debugTextView = findViewById(R.id.debug_text_view);
retryButton = findViewById(R.id.retry_button);
retryButton.setOnClickListener(this);
simpleExoPlayerView = findViewById(R.id.player_view);
simpleExoPlayerView.setControllerVisibilityListener(this);
simpleExoPlayerView.requestFocus();
playerView = findViewById(R.id.player_view);
playerView.setControllerVisibilityListener(this);
playerView.requestFocus();
}
@Override
@ -223,16 +220,14 @@ public class PlayerActivity extends Activity implements OnClickListener,
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// See whether the player view wants to handle media or DPAD keys events.
return simpleExoPlayerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
}
// OnClickListener methods
@Override
public void onClick(View view) {
if (view == retryButton) {
initializePlayer();
} else if (view.getParent() == debugRootView) {
if (view.getParent() == debugRootView) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
trackSelectionHelper.showSelectionDialog(
@ -241,6 +236,13 @@ public class PlayerActivity extends Activity implements OnClickListener,
}
}
// PlaybackControlView.PlaybackPreparer implementation
@Override
public void preparePlayback() {
initializePlayer();
}
// PlaybackControlView.VisibilityListener implementation
@Override
@ -273,9 +275,14 @@ public class PlayerActivity extends Activity implements OnClickListener,
try {
String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA
: DRM_SCHEME_UUID_EXTRA;
UUID drmSchemeUuid = DemoUtil.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl,
keyRequestPropertiesArray, multiSession);
UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
if (drmSchemeUuid == null) {
errorStringId = R.string.error_drm_unsupported_scheme;
} else {
drmSessionManager =
buildDrmSessionManagerV18(
drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession);
}
} catch (UnsupportedDrmException e) {
errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
@ -302,9 +309,10 @@ public class PlayerActivity extends Activity implements OnClickListener,
player.addMetadataOutput(eventLogger);
player.addAudioDebugListener(eventLogger);
player.addVideoDebugListener(eventLogger);
simpleExoPlayerView.setPlayer(player);
player.setPlayWhenReady(shouldAutoPlay);
playerView.setPlayer(player);
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
}
@ -345,9 +353,10 @@ public class PlayerActivity extends Activity implements OnClickListener,
releaseAdsLoader();
loadedAdTagUri = adTagUri;
}
try {
mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
} catch (Exception e) {
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
if (adsMediaSource != null) {
mediaSource = adsMediaSource;
} else {
showToast(R.string.ima_not_loaded);
}
} else {
@ -455,39 +464,47 @@ public class PlayerActivity extends Activity implements OnClickListener,
.buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
/**
* Returns an ads media source, reusing the ads loader if one exists.
*
* @throws Exception Thrown if it was not possible to create an ads media source, for example, due
* to a missing dependency.
*/
private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) throws Exception {
/** Returns an ads media source, reusing the ads loader if one exists. */
private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
// Load the extension source using reflection so the demo app doesn't have to depend on it.
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
if (adsLoader == null) {
adsLoader = (AdsLoader) loaderClass.getConstructor(Context.class, Uri.class)
.newInstance(this, adTagUri);
adUiViewGroup = new FrameLayout(this);
// The demo app has a non-null overlay frame layout.
simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup);
}
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
new AdsMediaSource.MediaSourceFactory() {
@Override
public MediaSource createMediaSource(
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
return PlayerActivity.this.buildMediaSource(
uri, /* overrideExtension= */ null, handler, listener);
}
try {
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
if (adsLoader == null) {
// Full class names used so the LINT.IfChange rule triggers should any of the classes move.
// LINT.IfChange
Constructor<? extends AdsLoader> loaderConstructor =
loaderClass
.asSubclass(AdsLoader.class)
.getConstructor(android.content.Context.class, android.net.Uri.class);
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
adsLoader = loaderConstructor.newInstance(this, adTagUri);
adUiViewGroup = new FrameLayout(this);
// The demo app has a non-null overlay frame layout.
playerView.getOverlayFrameLayout().addView(adUiViewGroup);
}
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
new AdsMediaSource.MediaSourceFactory() {
@Override
public MediaSource createMediaSource(
Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) {
return PlayerActivity.this.buildMediaSource(
uri, /* overrideExtension= */ null, handler, listener);
}
@Override
public int[] getSupportedTypes() {
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
}
};
return new AdsMediaSource(
mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
@Override
public int[] getSupportedTypes() {
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
}
};
return new AdsMediaSource(
mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger);
} catch (ClassNotFoundException e) {
// IMA extension not loaded.
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void releaseAdsLoader() {
@ -495,7 +512,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
adsLoader.release();
adsLoader = null;
loadedAdTagUri = null;
simpleExoPlayerView.getOverlayFrameLayout().removeAllViews();
playerView.getOverlayFrameLayout().removeAllViews();
}
}
@ -503,10 +520,6 @@ public class PlayerActivity extends Activity implements OnClickListener,
private void updateButtonVisibilities() {
debugRootView.removeAllViews();
retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE);
debugRootView.addView(retryButton);
if (player == null) {
return;
}

View file

@ -33,7 +33,6 @@ import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
@ -202,11 +201,9 @@ public class SampleChooserActivity extends Activity {
break;
case "drm_scheme":
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
try {
drmUuid = DemoUtil.getDrmUuid(reader.nextString());
} catch (UnsupportedDrmException e) {
throw new ParserException(e);
}
String drmScheme = reader.nextString();
drmUuid = Util.getDrmUuid(drmScheme);
Assertions.checkState(drmUuid != null, "Invalid drm_scheme: " + drmScheme);
break;
case "drm_license_url":
Assertions.checkState(!insidePlaylist,

View file

@ -0,0 +1,7 @@
# Proguard rules specific to the main demo app.
# Constructor accessed via reflection in PlayerActivity
-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
<init>(android.content.Context, android.net.Uri);
}

View file

@ -20,7 +20,7 @@
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
@ -42,15 +42,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<Button android:id="@+id/retry_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/retry"
android:visibility="gone"/>
</LinearLayout>
android:visibility="gone"/>
</LinearLayout>

View file

@ -23,8 +23,6 @@
<string name="text">Text</string>
<string name="retry">Retry</string>
<string name="selection_disabled">Disabled</string>
<string name="selection_default">Default</string>

30
extensions/cast/README.md Normal file
View file

@ -0,0 +1,30 @@
# ExoPlayer Cast extension #
## Description ##
The cast extension is a [Player][] implementation that controls playback on a
Cast receiver app.
[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html
## Getting the extension ##
The easiest way to use the extension is to add it as a gradle dependency:
```gradle
compile 'com.google.android.exoplayer:extension-cast:rX.X.X'
```
where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used.
Alternatively, you can clone the ExoPlayer repository and depend on the module
locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
## Using the extension ##
Create a `CastPlayer` and use it to integrate Cast into your app using
ExoPlayer's common `Player` interface.

View file

@ -0,0 +1,56 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 14
targetSdkVersion project.ext.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}
dependencies {
// This dependency is necessary to force the supportLibraryVersion of
// com.android.support:support-v4 to be used. Else an older version (25.2.0)
// is included via:
// com.google.android.gms:play-services-cast-framework:11.4.2
// |-- com.google.android.gms:play-services-basement:11.4.2
// |-- com.android.support:support-v4:25.2.0
compile 'com.android.support:support-v4:' + supportLibraryVersion
compile 'com.android.support:appcompat-v7:' + supportLibraryVersion
compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion
compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion
compile project(modulePrefix + 'library-core')
compile project(modulePrefix + 'library-ui')
testCompile project(modulePrefix + 'testutils')
testCompile 'junit:junit:' + junitVersion
testCompile 'org.mockito:mockito-core:' + mockitoVersion
testCompile 'org.robolectric:robolectric:' + robolectricVersion
}
ext {
javadocTitle = 'Cast extension'
}
apply from: '../../javadoc_library.gradle'
ext {
releaseArtifact = 'extension-cast'
releaseDescription = 'Cast extension for ExoPlayer.'
}
apply from: '../../publish.gradle'

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest package="com.google.android.exoplayer2.ext.cast"/>

View file

@ -0,0 +1,882 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.android.gms.cast.CastStatusCodes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.MediaTrack;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.SessionManager;
import com.google.android.gms.cast.framework.SessionManagerListener;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* {@link Player} implementation that communicates with a Cast receiver app.
*
* <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
* Cast context passed to {@link #CastPlayer}. To keep track of the session,
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
* implemented and attached to the player.</p>
*
* <p> If no session is available, the player state will remain unchanged and calls to methods that
* alter it will be ignored. Querying the player state is possible even when no session is
* available, in which case, the last observed receiver app state is reported.</p>
*
* <p>Methods should be called on the application's main thread.</p>
*/
public final class CastPlayer implements Player {
/**
* Listener of changes in the cast session availability.
*/
public interface SessionAvailabilityListener {
/**
* Called when a cast session becomes available to the player.
*/
void onCastSessionAvailable();
/**
* Called when the cast session becomes unavailable.
*/
void onCastSessionUnavailable();
}
private static final String TAG = "CastPlayer";
private static final int RENDERER_COUNT = 3;
private static final int RENDERER_INDEX_VIDEO = 0;
private static final int RENDERER_INDEX_AUDIO = 1;
private static final int RENDERER_INDEX_TEXT = 2;
private static final long PROGRESS_REPORT_PERIOD_MS = 1000;
private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY =
new TrackSelectionArray(null, null, null);
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
private final CastContext castContext;
// TODO: Allow custom implementations of CastTimelineTracker.
private final CastTimelineTracker timelineTracker;
private final Timeline.Window window;
private final Timeline.Period period;
private RemoteMediaClient remoteMediaClient;
// Result callbacks.
private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback;
// Listeners.
private final CopyOnWriteArraySet<EventListener> listeners;
private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection;
private int playbackState;
private int repeatMode;
private int currentWindowIndex;
private boolean playWhenReady;
private long lastReportedPositionMs;
private int pendingSeekCount;
private int pendingSeekWindowIndex;
private long pendingSeekPositionMs;
private boolean waitingForInitialTimeline;
/**
* @param castContext The context from which the cast session is obtained.
*/
public CastPlayer(CastContext castContext) {
this.castContext = castContext;
timelineTracker = new CastTimelineTracker();
window = new Timeline.Window();
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
listeners = new CopyOnWriteArraySet<>();
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
CastSession session = sessionManager.getCurrentCastSession();
remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;
playbackState = STATE_IDLE;
repeatMode = REPEAT_MODE_OFF;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = TrackGroupArray.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
updateInternalState();
}
// Media Queue manipulation methods.
/**
* Loads a single item media queue. If no session is available, does nothing.
*
* @param item The item to load.
* @param positionMs The position at which the playback should start in milliseconds relative to
* the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback
* starts at position 0.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
}
/**
* Loads a media queue. If no session is available, does nothing.
*
* @param items The items to load.
* @param startIndex The index of the item at which playback should start.
* @param positionMs The position at which the playback should start in milliseconds relative to
* the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback
* starts at position 0.
* @param repeatMode The repeat mode for the created media queue.
* @return The Cast {@code PendingResult}, or null if no session is available.
*/
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex,
long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
waitingForInitialTimeline = true;
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
positionMs, null);
}
return null;
}
/**
* Appends a sequence of items to the media queue. If no media queue exists, does nothing.
*
* @param items The items to append.
* @return The Cast {@code PendingResult}, or null if no media queue exists.
*/
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
}
/**
* Inserts a sequence of items into the media queue. If no media queue or period with id {@code
* periodId} exist, does nothing.
*
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
* that will follow immediately after the inserted items.
* @param items The items to insert.
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
return remoteMediaClient.queueInsertItems(items, periodId, null);
}
return null;
}
/**
* Removes an item from the media queue. If no media queue or period with id {@code periodId}
* exist, does nothing.
*
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
* to remove.
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
public PendingResult<MediaChannelResult> removeItem(int periodId) {
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueRemoveItem(periodId, null);
}
return null;
}
/**
* Moves an existing item within the media queue. If no media queue or period with id {@code
* periodId} exist, does nothing.
*
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
* to move.
* @param newIndex The target index of the item in the media queue. Must be in the range 0 &lt;=
* index &lt; {@link Timeline#getPeriodCount()}, as provided by {@link #getCurrentTimeline()}.
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist.
*/
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null);
}
return null;
}
/**
* Returns the item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist.
*
* @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item
* to get.
* @return The item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist.
*/
public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
? mediaStatus.getItemById(periodId) : null;
}
// CastSession methods.
/**
* Returns whether a cast session is available.
*/
public boolean isCastSessionAvailable() {
return remoteMediaClient != null;
}
/**
* Sets a listener for updates on the cast session availability.
*
* @param listener The {@link SessionAvailabilityListener}.
*/
public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
sessionAvailabilityListener = listener;
}
// Player implementation.
@Override
public VideoComponent getVideoComponent() {
return null;
}
@Override
public TextComponent getTextComponent() {
return null;
}
@Override
public void addListener(EventListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(EventListener listener) {
listeners.remove(listener);
}
@Override
public int getPlaybackState() {
return playbackState;
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
if (remoteMediaClient == null) {
return;
}
if (playWhenReady) {
remoteMediaClient.play();
} else {
remoteMediaClient.pause();
}
}
@Override
public boolean getPlayWhenReady() {
return playWhenReady;
}
@Override
public void seekToDefaultPosition() {
seekTo(0);
}
@Override
public void seekToDefaultPosition(int windowIndex) {
seekTo(windowIndex, 0);
}
@Override
public void seekTo(long positionMs) {
seekTo(getCurrentWindowIndex(), positionMs);
}
@Override
public void seekTo(int windowIndex, long positionMs) {
MediaStatus mediaStatus = getMediaStatus();
// We assume the default position is 0. There is no support for seeking to the default position
// in RemoteMediaClient.
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
if (mediaStatus != null) {
if (getCurrentWindowIndex() != windowIndex) {
remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid,
positionMs, null).setResultCallback(seekResultCallback);
} else {
remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback);
}
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
}
} else if (pendingSeekCount == 0) {
for (EventListener listener : listeners) {
listener.onSeekProcessed();
}
}
}
@Override
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
// Unsupported by the RemoteMediaClient API. Do nothing.
}
@Override
public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT;
}
@Override
public void stop() {
stop(/* reset= */ false);
}
@Override
public void stop(boolean reset) {
playbackState = STATE_IDLE;
if (remoteMediaClient != null) {
// TODO(b/69792021): Support or emulate stop without position reset.
remoteMediaClient.stop();
}
}
@Override
public void release() {
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.removeSessionManagerListener(statusListener, CastSession.class);
sessionManager.endCurrentSession(false);
}
@Override
public int getRendererCount() {
// We assume there are three renderers: video, audio, and text.
return RENDERER_COUNT;
}
@Override
public int getRendererType(int index) {
switch (index) {
case RENDERER_INDEX_VIDEO:
return C.TRACK_TYPE_VIDEO;
case RENDERER_INDEX_AUDIO:
return C.TRACK_TYPE_AUDIO;
case RENDERER_INDEX_TEXT:
return C.TRACK_TYPE_TEXT;
default:
throw new IndexOutOfBoundsException();
}
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient != null) {
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null);
}
}
@Override
@RepeatMode public int getRepeatMode() {
return repeatMode;
}
@Override
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
// TODO: Support shuffle mode.
}
@Override
public boolean getShuffleModeEnabled() {
// TODO: Support shuffle mode.
return false;
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return currentTrackSelection;
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return currentTrackGroups;
}
@Override
public Timeline getCurrentTimeline() {
return currentTimeline;
}
@Override
@Nullable public Object getCurrentManifest() {
return null;
}
@Override
public int getCurrentPeriodIndex() {
return getCurrentWindowIndex();
}
@Override
public int getCurrentWindowIndex() {
return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
}
@Override
public int getNextWindowIndex() {
return currentTimeline.isEmpty() ? C.INDEX_UNSET
: currentTimeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, false);
}
@Override
public int getPreviousWindowIndex() {
return currentTimeline.isEmpty() ? C.INDEX_UNSET
: currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false);
}
// TODO: Fill the cast timeline information with ProgressListener's duration updates.
// See [Internal: b/65152553].
@Override
public long getDuration() {
return currentTimeline.isEmpty() ? C.TIME_UNSET
: currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
}
@Override
public long getCurrentPosition() {
return pendingSeekPositionMs != C.TIME_UNSET
? pendingSeekPositionMs
: remoteMediaClient != null
? remoteMediaClient.getApproximateStreamPosition()
: lastReportedPositionMs;
}
@Override
public long getBufferedPosition() {
return getCurrentPosition();
}
@Override
public int getBufferedPercentage() {
long position = getBufferedPosition();
long duration = getDuration();
return position == C.TIME_UNSET || duration == C.TIME_UNSET
? 0
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
}
@Override
public boolean isCurrentWindowDynamic() {
return !currentTimeline.isEmpty()
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
}
@Override
public boolean isCurrentWindowSeekable() {
return !currentTimeline.isEmpty()
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
}
@Override
public boolean isPlayingAd() {
return false;
}
@Override
public int getCurrentAdGroupIndex() {
return C.INDEX_UNSET;
}
@Override
public int getCurrentAdIndexInAdGroup() {
return C.INDEX_UNSET;
}
@Override
public boolean isLoading() {
return false;
}
@Override
public long getContentPosition() {
return getCurrentPosition();
}
// Internal methods.
public void updateInternalState() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return;
}
int playbackState = fetchPlaybackState(remoteMediaClient);
boolean playWhenReady = !remoteMediaClient.isPaused();
if (this.playbackState != playbackState
|| this.playWhenReady != playWhenReady) {
this.playbackState = playbackState;
this.playWhenReady = playWhenReady;
for (EventListener listener : listeners) {
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
}
}
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
for (EventListener listener : listeners) {
listener.onRepeatModeChanged(repeatMode);
}
}
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex;
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
}
}
if (updateTracksAndSelections()) {
for (EventListener listener : listeners) {
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
}
}
maybeUpdateTimelineAndNotify();
}
private void maybeUpdateTimelineAndNotify() {
if (updateTimeline()) {
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
waitingForInitialTimeline = false;
for (EventListener listener : listeners) {
listener.onTimelineChanged(currentTimeline, null, reason);
}
}
}
/**
* Updates the current timeline and returns whether it has changed.
*/
private boolean updateTimeline() {
CastTimeline oldTimeline = currentTimeline;
MediaStatus status = getMediaStatus();
currentTimeline =
status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
return !oldTimeline.equals(currentTimeline);
}
/**
* Updates the internal tracks and selection and returns whether they have changed.
*/
private boolean updateTracksAndSelections() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
return false;
}
MediaStatus mediaStatus = getMediaStatus();
MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;
List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;
if (castMediaTracks == null || castMediaTracks.isEmpty()) {
boolean hasChanged = !currentTrackGroups.isEmpty();
currentTrackGroups = TrackGroupArray.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
return hasChanged;
}
long[] activeTrackIds = mediaStatus.getActiveTrackIds();
if (activeTrackIds == null) {
activeTrackIds = EMPTY_TRACK_ID_ARRAY;
}
TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()];
TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
for (int i = 0; i < castMediaTracks.size(); i++) {
MediaTrack mediaTrack = castMediaTracks.get(i);
trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack));
long id = mediaTrack.getId();
int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
int rendererIndex = getRendererIndexForTrackType(trackType);
if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET
&& trackSelections[rendererIndex] == null) {
trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0);
}
}
TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);
TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections);
if (!newTrackGroups.equals(currentTrackGroups)
|| !newTrackSelections.equals(currentTrackSelection)) {
currentTrackSelection = new TrackSelectionArray(trackSelections);
currentTrackGroups = new TrackGroupArray(trackGroups);
return true;
}
return false;
}
private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
if (this.remoteMediaClient == remoteMediaClient) {
// Do nothing.
return;
}
if (this.remoteMediaClient != null) {
this.remoteMediaClient.removeListener(statusListener);
this.remoteMediaClient.removeProgressListener(statusListener);
}
this.remoteMediaClient = remoteMediaClient;
if (remoteMediaClient != null) {
if (sessionAvailabilityListener != null) {
sessionAvailabilityListener.onCastSessionAvailable();
}
remoteMediaClient.addListener(statusListener);
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
updateInternalState();
} else {
if (sessionAvailabilityListener != null) {
sessionAvailabilityListener.onCastSessionUnavailable();
}
}
}
private @Nullable MediaStatus getMediaStatus() {
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
}
/**
* Retrieves the playback state from {@code remoteMediaClient} and maps it into a {@link Player}
* state
*/
private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) {
int receiverAppStatus = remoteMediaClient.getPlayerState();
switch (receiverAppStatus) {
case MediaStatus.PLAYER_STATE_BUFFERING:
return STATE_BUFFERING;
case MediaStatus.PLAYER_STATE_PLAYING:
case MediaStatus.PLAYER_STATE_PAUSED:
return STATE_READY;
case MediaStatus.PLAYER_STATE_IDLE:
case MediaStatus.PLAYER_STATE_UNKNOWN:
default:
return STATE_IDLE;
}
}
/**
* Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a
* {@link Player.RepeatMode}.
*/
@RepeatMode
private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) {
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
if (mediaStatus == null) {
// No media session active, yet.
return REPEAT_MODE_OFF;
}
int castRepeatMode = mediaStatus.getQueueRepeatMode();
switch (castRepeatMode) {
case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
return REPEAT_MODE_ONE;
case MediaStatus.REPEAT_MODE_REPEAT_ALL:
case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
return REPEAT_MODE_ALL;
case MediaStatus.REPEAT_MODE_REPEAT_OFF:
return REPEAT_MODE_OFF;
default:
throw new IllegalStateException();
}
}
/**
* Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
* there is no media session, returns 0.
*/
private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
Integer currentItemId = mediaStatus != null
? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
return currentItemId != null ? currentItemId : 0;
}
private static boolean isTrackActive(long id, long[] activeTrackIds) {
for (long activeTrackId : activeTrackIds) {
if (activeTrackId == id) {
return true;
}
}
return false;
}
private static int getRendererIndexForTrackType(int trackType) {
return trackType == C.TRACK_TYPE_VIDEO
? RENDERER_INDEX_VIDEO
: trackType == C.TRACK_TYPE_AUDIO
? RENDERER_INDEX_AUDIO
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : C.INDEX_UNSET;
}
private static int getCastRepeatMode(@RepeatMode int repeatMode) {
switch (repeatMode) {
case REPEAT_MODE_ONE:
return MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
case REPEAT_MODE_ALL:
return MediaStatus.REPEAT_MODE_REPEAT_ALL;
case REPEAT_MODE_OFF:
return MediaStatus.REPEAT_MODE_REPEAT_OFF;
default:
throw new IllegalArgumentException();
}
}
private final class StatusListener implements RemoteMediaClient.Listener,
SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
// RemoteMediaClient.ProgressListener implementation.
@Override
public void onProgressUpdated(long progressMs, long unusedDurationMs) {
lastReportedPositionMs = progressMs;
}
// RemoteMediaClient.Listener implementation.
@Override
public void onStatusUpdated() {
updateInternalState();
}
@Override
public void onMetadataUpdated() {}
@Override
public void onQueueStatusUpdated() {
maybeUpdateTimelineAndNotify();
}
@Override
public void onPreloadStatusUpdated() {}
@Override
public void onSendingRemoteMediaRequest() {}
@Override
public void onAdBreakStatusUpdated() {}
// SessionManagerListener implementation.
@Override
public void onSessionStarted(CastSession castSession, String s) {
setRemoteMediaClient(castSession.getRemoteMediaClient());
}
@Override
public void onSessionResumed(CastSession castSession, boolean b) {
setRemoteMediaClient(castSession.getRemoteMediaClient());
}
@Override
public void onSessionEnded(CastSession castSession, int i) {
setRemoteMediaClient(null);
}
@Override
public void onSessionSuspended(CastSession castSession, int i) {
setRemoteMediaClient(null);
}
@Override
public void onSessionResumeFailed(CastSession castSession, int statusCode) {
Log.e(TAG, "Session resume failed. Error code " + statusCode + ": "
+ CastUtils.getLogString(statusCode));
}
@Override
public void onSessionStarting(CastSession castSession) {
// Do nothing.
}
@Override
public void onSessionStartFailed(CastSession castSession, int statusCode) {
Log.e(TAG, "Session start failed. Error code " + statusCode + ": "
+ CastUtils.getLogString(statusCode));
}
@Override
public void onSessionEnding(CastSession castSession) {
// Do nothing.
}
@Override
public void onSessionResuming(CastSession castSession, String s) {
// Do nothing.
}
}
// Result callbacks hooks.
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
@Override
public void onResult(@NonNull MediaChannelResult result) {
int statusCode = result.getStatus().getStatusCode();
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
+ CastUtils.getLogString(statusCode));
}
if (--pendingSeekCount == 0) {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
for (EventListener listener : listeners) {
listener.onSeekProcessed();
}
}
}
}
}

View file

@ -0,0 +1,123 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* A {@link Timeline} for Cast media queues.
*/
/* package */ final class CastTimeline extends Timeline {
public static final CastTimeline EMPTY_CAST_TIMELINE =
new CastTimeline(
Collections.<MediaQueueItem>emptyList(), Collections.<String, Long>emptyMap());
private final SparseIntArray idsToIndex;
private final int[] ids;
private final long[] durationsUs;
private final long[] defaultPositionsUs;
/**
* @param items A list of cast media queue items to represent.
* @param contentIdToDurationUsMap A map of content id to duration in microseconds.
*/
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) {
int itemCount = items.size();
int index = 0;
idsToIndex = new SparseIntArray(itemCount);
ids = new int[itemCount];
durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount];
for (MediaQueueItem item : items) {
int itemId = item.getItemId();
ids[index] = itemId;
idsToIndex.put(itemId, index);
MediaInfo mediaInfo = item.getMedia();
String contentId = mediaInfo.getContentId();
durationsUs[index] =
contentIdToDurationUsMap.containsKey(contentId)
? contentIdToDurationUsMap.get(contentId)
: CastUtils.getStreamDurationUs(mediaInfo);
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
index++;
}
}
// Timeline implementation.
@Override
public int getWindowCount() {
return ids.length;
}
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET;
return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic,
defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0);
}
@Override
public int getPeriodCount() {
return ids.length;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
int id = ids[periodIndex];
return period.set(id, id, periodIndex, durationsUs[periodIndex], 0);
}
@Override
public int getIndexOfPeriod(Object uid) {
return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
}
// equals and hashCode implementations.
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (!(other instanceof CastTimeline)) {
return false;
}
CastTimeline that = (CastTimeline) other;
return Arrays.equals(ids, that.ids)
&& Arrays.equals(durationsUs, that.durationsUs)
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(ids);
result = 31 * result + Arrays.hashCode(durationsUs);
result = 31 * result + Arrays.hashCode(defaultPositionsUs);
return result;
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* Creates {@link CastTimeline}s from cast receiver app media status.
*
* <p>This class keeps track of the duration reported by the current item to fill any missing
* durations in the media queue items [See internal: b/65152553].
*/
/* package */ final class CastTimelineTracker {
private final HashMap<String, Long> contentIdToDurationUsMap;
private final HashSet<String> scratchContentIdSet;
public CastTimelineTracker() {
contentIdToDurationUsMap = new HashMap<>();
scratchContentIdSet = new HashSet<>();
}
/**
* Returns a {@link CastTimeline} that represent the given {@code status}.
*
* @param status The Cast media status.
* @return A {@link CastTimeline} that represent the given {@code status}.
*/
public CastTimeline getCastTimeline(MediaStatus status) {
MediaInfo mediaInfo = status.getMediaInfo();
List<MediaQueueItem> items = status.getQueueItems();
removeUnusedDurationEntries(items);
if (mediaInfo != null) {
String contentId = mediaInfo.getContentId();
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
contentIdToDurationUsMap.put(contentId, durationUs);
}
return new CastTimeline(items, contentIdToDurationUsMap);
}
private void removeUnusedDurationEntries(List<MediaQueueItem> items) {
scratchContentIdSet.clear();
for (MediaQueueItem item : items) {
scratchContentIdSet.add(item.getMedia().getContentId());
}
contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.gms.cast.CastStatusCodes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaTrack;
/**
* Utility methods for ExoPlayer/Cast integration.
*/
/* package */ final class CastUtils {
/**
* Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
* unknown or not applicable.
*
* @param mediaInfo The media info to get the duration from.
* @return The duration in microseconds.
*/
public static long getStreamDurationUs(MediaInfo mediaInfo) {
long durationMs =
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
}
/**
* Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
* {@link CastStatusCodes}.
*
* @param statusCode A Cast API status code.
* @return A descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
* {@link CastStatusCodes}.
*/
public static String getLogString(int statusCode) {
switch (statusCode) {
case CastStatusCodes.APPLICATION_NOT_FOUND:
return "A requested application could not be found.";
case CastStatusCodes.APPLICATION_NOT_RUNNING:
return "A requested application is not currently running.";
case CastStatusCodes.AUTHENTICATION_FAILED:
return "Authentication failure.";
case CastStatusCodes.CANCELED:
return "An in-progress request has been canceled, most likely because another action has "
+ "preempted it.";
case CastStatusCodes.ERROR_SERVICE_CREATION_FAILED:
return "The Cast Remote Display service could not be created.";
case CastStatusCodes.ERROR_SERVICE_DISCONNECTED:
return "The Cast Remote Display service was disconnected.";
case CastStatusCodes.FAILED:
return "The in-progress request failed.";
case CastStatusCodes.INTERNAL_ERROR:
return "An internal error has occurred.";
case CastStatusCodes.INTERRUPTED:
return "A blocking call was interrupted while waiting and did not run to completion.";
case CastStatusCodes.INVALID_REQUEST:
return "An invalid request was made.";
case CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL:
return "A message could not be sent because there is not enough room in the send buffer at "
+ "this time.";
case CastStatusCodes.MESSAGE_TOO_LARGE:
return "A message could not be sent because it is too large.";
case CastStatusCodes.NETWORK_ERROR:
return "Network I/O error.";
case CastStatusCodes.NOT_ALLOWED:
return "The request was disallowed and could not be completed.";
case CastStatusCodes.REPLACED:
return "The request's progress is no longer being tracked because another request of the "
+ "same type has been made before the first request completed.";
case CastStatusCodes.SUCCESS:
return "Success.";
case CastStatusCodes.TIMEOUT:
return "An operation has timed out.";
case CastStatusCodes.UNKNOWN_ERROR:
return "An unknown, unexpected error has occurred.";
default:
return "Unknown: " + statusCode;
}
}
/**
* Creates a {@link Format} instance containing all information contained in the given
* {@link MediaTrack} object.
*
* @param mediaTrack The {@link MediaTrack}.
* @return The equivalent {@link Format}.
*/
public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
}
private CastUtils() {}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import android.content.Context;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;
import java.util.List;
/**
* A convenience {@link OptionsProvider} to target the default cast receiver app.
*/
public final class DefaultCastOptionsProvider implements OptionsProvider {
@Override
public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder()
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
.setStopReceiverApplicationWhenEndingSession(true).build();
}
@Override
public List<SessionProvider> getAdditionalSessionProviders(Context context) {
return null;
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
import java.util.ArrayList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Tests for {@link CastTimelineTracker}. */
@RunWith(RobolectricTestRunner.class)
@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
public class CastTimelineTrackerTest {
private static final long DURATION_1_MS = 1000;
private static final long DURATION_2_MS = 2000;
private static final long DURATION_3_MS = 3000;
private static final long DURATION_4_MS = 4000;
private static final long DURATION_5_MS = 5000;
/** Tests that duration of the current media info is correctly propagated to the timeline. */
@Test
public void testGetCastTimeline() {
MediaInfo mediaInfo;
MediaStatus status =
mockMediaStatus(
new int[] {1, 2, 3},
new String[] {"contentId1", "contentId2", "contentId3"},
new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
CastTimelineTracker tracker = new CastTimelineTracker();
mediaInfo = mockMediaInfo("contentId1", DURATION_1_MS);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status),
C.msToUs(DURATION_1_MS),
C.TIME_UNSET,
C.msToUs(DURATION_3_MS));
mediaInfo = mockMediaInfo("contentId2", DURATION_2_MS);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status),
C.msToUs(DURATION_1_MS),
C.msToUs(DURATION_2_MS),
C.msToUs(DURATION_3_MS));
MediaStatus newStatus =
mockMediaStatus(
new int[] {4, 1, 5, 3},
new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
new long[] {
MediaInfo.UNKNOWN_DURATION,
MediaInfo.UNKNOWN_DURATION,
DURATION_5_MS,
MediaInfo.UNKNOWN_DURATION
});
mediaInfo = mockMediaInfo("contentId5", DURATION_5_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus),
C.TIME_UNSET,
C.msToUs(DURATION_1_MS),
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS));
mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus),
C.TIME_UNSET,
C.msToUs(DURATION_1_MS),
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS));
mediaInfo = mockMediaInfo("contentId4", DURATION_4_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus),
C.msToUs(DURATION_4_MS),
C.msToUs(DURATION_1_MS),
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS));
}
private static MediaStatus mockMediaStatus(
int[] itemIds, String[] contentIds, long[] durationsMs) {
ArrayList<MediaQueueItem> items = new ArrayList<>();
for (int i = 0; i < contentIds.length; i++) {
MediaInfo mediaInfo = mockMediaInfo(contentIds[i], durationsMs[i]);
MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
Mockito.when(item.getMedia()).thenReturn(mediaInfo);
Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
items.add(item);
}
MediaStatus status = Mockito.mock(MediaStatus.class);
Mockito.when(status.getQueueItems()).thenReturn(items);
return status;
}
private static MediaInfo mockMediaInfo(String contentId, long durationMs) {
MediaInfo mediaInfo = Mockito.mock(MediaInfo.class);
Mockito.when(mediaInfo.getContentId()).thenReturn(contentId);
Mockito.when(mediaInfo.getStreamDuration()).thenReturn(durationMs);
return mediaInfo;
}
}

View file

@ -19,10 +19,20 @@ and enable the extension:
1. Copy the three jar files into the `libs` directory of this extension
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
directory of this extension
* In your `settings.gradle` file, add
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
applies `core_settings.gradle`.
1. In your `settings.gradle` file, add
`gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
applies `core_settings.gradle`.
1. In all `build.gradle` files where this extension is linked as a dependency,
add
```
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
```
to enable Java 8 features required by the Cronet library.
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android

View file

@ -16,7 +16,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer.ext.cronet">
package="com.google.android.exoplayer2.ext.cronet">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="27"/>
@ -28,6 +28,6 @@
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.google.android.exoplayer.ext.cronet"/>
android:targetPackage="com.google.android.exoplayer2.ext.cronet"/>
</manifest>

View file

@ -15,8 +15,7 @@
*/
package com.google.android.exoplayer2.ext.cronet;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -53,13 +52,13 @@ public final class ByteArrayUploadDataProviderTest {
@Test
public void testGetLength() {
assertEquals(TEST_DATA.length, byteArrayUploadDataProvider.getLength());
assertThat(byteArrayUploadDataProvider.getLength()).isEqualTo(TEST_DATA.length);
}
@Test
public void testReadFullBuffer() throws IOException {
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(TEST_DATA, byteBuffer.array());
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
}
@Test
@ -69,12 +68,12 @@ public final class ByteArrayUploadDataProviderTest {
byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);
// Read half of the data.
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(firstHalf, byteBuffer.array());
assertThat(byteBuffer.array()).isEqualTo(firstHalf);
// Read the second half of the data.
byteBuffer.rewind();
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(secondHalf, byteBuffer.array());
assertThat(byteBuffer.array()).isEqualTo(secondHalf);
verify(mockUploadDataSink, times(2)).onReadSucceeded(false);
}
@ -82,13 +81,13 @@ public final class ByteArrayUploadDataProviderTest {
public void testRewind() throws IOException {
// Read all the data.
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(TEST_DATA, byteBuffer.array());
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
// Rewind and make sure it can be read again.
byteBuffer.clear();
byteArrayUploadDataProvider.rewind(mockUploadDataSink);
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
assertArrayEquals(TEST_DATA, byteBuffer.array());
assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
verify(mockUploadDataSink).onRewindSucceeded();
}

View file

@ -15,10 +15,7 @@
*/
package com.google.android.exoplayer2.ext.cronet;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
@ -222,7 +219,7 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpen() throws HttpDataSourceException {
mockResponseStartSuccess();
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testDataSpec));
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@ -234,7 +231,7 @@ public final class CronetDataSourceTest {
testResponseHeader.put("Content-Length", Long.toString(50L));
mockResponseStartSuccess();
assertEquals(5000 /* contentLength */, dataSourceUnderTest.open(testDataSpec));
assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@ -247,7 +244,7 @@ public final class CronetDataSourceTest {
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
// Check for connection not automatically closed.
assertFalse(e.getCause() instanceof UnknownHostException);
assertThat(e.getCause() instanceof UnknownHostException).isFalse();
verify(mockUrlRequest, never()).cancel();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@ -264,7 +261,7 @@ public final class CronetDataSourceTest {
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
// Check for connection not automatically closed.
assertTrue(e.getCause() instanceof UnknownHostException);
assertThat(e.getCause() instanceof UnknownHostException).isTrue();
verify(mockUrlRequest, never()).cancel();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@ -279,7 +276,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
assertTrue(e instanceof HttpDataSource.InvalidResponseCodeException);
assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue();
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
@ -295,7 +292,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
assertTrue(e instanceof HttpDataSource.InvalidContentTypeException);
assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
@ -307,7 +304,7 @@ public final class CronetDataSourceTest {
mockResponseStartSuccess();
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testPostDataSpec));
assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec);
}
@ -346,13 +343,13 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[8];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
assertEquals(8, bytesRead);
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
assertThat(bytesRead).isEqualTo(8);
returnedBuffer = new byte[8];
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertArrayEquals(buildTestDataArray(8, 8), returnedBuffer);
assertEquals(8, bytesRead);
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(8, 8));
assertThat(bytesRead).isEqualTo(8);
// Should have only called read on cronet once.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
@ -378,11 +375,11 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.open(testDataSpec);
returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
assertEquals(10, bytesRead);
assertThat(bytesRead).isEqualTo(10);
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
assertEquals(6, bytesRead);
assertThat(bytesRead).isEqualTo(6);
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
assertEquals(C.RESULT_END_OF_INPUT, bytesRead);
assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
}
@Test
@ -394,8 +391,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
assertEquals(8, bytesRead);
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
assertThat(bytesRead).isEqualTo(8);
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
}
@ -410,8 +407,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
assertEquals(16, bytesRead);
assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer);
assertThat(bytesRead).isEqualTo(16);
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
}
@ -426,8 +423,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
assertEquals(16, bytesRead);
assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer);
assertThat(bytesRead).isEqualTo(16);
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
}
@ -441,8 +438,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
assertEquals(8, bytesRead);
assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
assertThat(bytesRead).isEqualTo(8);
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
}
@ -455,8 +452,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[24];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
assertArrayEquals(suffixZeros(buildTestDataArray(0, 16), 24), returnedBuffer);
assertEquals(16, bytesRead);
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));
assertThat(bytesRead).isEqualTo(16);
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
}
@ -470,8 +467,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[8];
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
assertEquals(8, bytesRead);
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
assertThat(bytesRead).isEqualTo(8);
dataSourceUnderTest.close();
verify(mockTransferListener).onTransferEnd(dataSourceUnderTest);
@ -484,7 +481,7 @@ public final class CronetDataSourceTest {
}
// 16 bytes were attempted but only 8 should have been successfully read.
assertEquals(8, bytesRead);
assertThat(bytesRead).isEqualTo(8);
}
@Test
@ -498,20 +495,20 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[8];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertEquals(8, bytesRead);
assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
assertThat(bytesRead).isEqualTo(8);
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
// The current buffer is kept if not completely consumed by DataSource reader.
returnedBuffer = new byte[8];
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);
assertEquals(14, bytesRead);
assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer);
assertThat(bytesRead).isEqualTo(14);
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(8, 6), 8));
// 2 bytes left at this point.
returnedBuffer = new byte[8];
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
assertEquals(16, bytesRead);
assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer);
assertThat(bytesRead).isEqualTo(16);
assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(14, 2), 8));
// Should have only called read on cronet once.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
@ -524,8 +521,8 @@ public final class CronetDataSourceTest {
// Return C.RESULT_END_OF_INPUT
returnedBuffer = new byte[16];
int bytesOverRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
assertEquals(C.RESULT_END_OF_INPUT, bytesOverRead);
assertArrayEquals(new byte[16], returnedBuffer);
assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
assertThat(returnedBuffer).isEqualTo(new byte[16]);
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
verify(mockTransferListener, never()).onBytesTransferred(dataSourceUnderTest,
C.RESULT_END_OF_INPUT);
@ -533,7 +530,7 @@ public final class CronetDataSourceTest {
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
assertEquals(16, bytesRead);
assertThat(bytesRead).isEqualTo(16);
}
@Test
@ -550,11 +547,10 @@ public final class CronetDataSourceTest {
fail();
} catch (HttpDataSourceException e) {
// Expected.
assertTrue(e instanceof CronetDataSource.OpenException);
assertTrue(e.getCause() instanceof SocketTimeoutException);
assertEquals(
TEST_CONNECTION_STATUS,
((CronetDataSource.OpenException) e).cronetConnectionStatus);
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
.isEqualTo(TEST_CONNECTION_STATUS);
timedOutCondition.open();
}
}
@ -562,10 +558,10 @@ public final class CronetDataSourceTest {
startCondition.block();
// We should still be trying to open.
assertFalse(timedOutCondition.block(50));
assertThat(timedOutCondition.block(50)).isFalse();
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(timedOutCondition.block(50));
assertThat(timedOutCondition.block(50)).isFalse();
// Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS);
timedOutCondition.block();
@ -588,11 +584,10 @@ public final class CronetDataSourceTest {
fail();
} catch (HttpDataSourceException e) {
// Expected.
assertTrue(e instanceof CronetDataSource.OpenException);
assertTrue(e.getCause() instanceof CronetDataSource.InterruptedIOException);
assertEquals(
TEST_INVALID_CONNECTION_STATUS,
((CronetDataSource.OpenException) e).cronetConnectionStatus);
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
.isEqualTo(TEST_INVALID_CONNECTION_STATUS);
timedOutCondition.open();
}
}
@ -601,10 +596,10 @@ public final class CronetDataSourceTest {
startCondition.block();
// We should still be trying to open.
assertFalse(timedOutCondition.block(50));
assertThat(timedOutCondition.block(50)).isFalse();
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(timedOutCondition.block(50));
assertThat(timedOutCondition.block(50)).isFalse();
// Now we interrupt.
thread.interrupt();
timedOutCondition.block();
@ -632,10 +627,10 @@ public final class CronetDataSourceTest {
startCondition.block();
// We should still be trying to open.
assertFalse(openCondition.block(50));
assertThat(openCondition.block(50)).isFalse();
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(openCondition.block(50));
assertThat(openCondition.block(50)).isFalse();
// The response arrives just in time.
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
openCondition.block();
@ -656,8 +651,8 @@ public final class CronetDataSourceTest {
fail();
} catch (HttpDataSourceException e) {
// Expected.
assertTrue(e instanceof CronetDataSource.OpenException);
assertTrue(e.getCause() instanceof SocketTimeoutException);
assertThat(e instanceof CronetDataSource.OpenException).isTrue();
assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
openExceptions.getAndIncrement();
timedOutCondition.open();
}
@ -666,10 +661,10 @@ public final class CronetDataSourceTest {
startCondition.block();
// We should still be trying to open.
assertFalse(timedOutCondition.block(50));
assertThat(timedOutCondition.block(50)).isFalse();
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
assertFalse(timedOutCondition.block(50));
assertThat(timedOutCondition.block(50)).isFalse();
// A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
"RandomRedirectedUrl1");
@ -677,9 +672,9 @@ public final class CronetDataSourceTest {
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
// Give the thread some time to run.
assertFalse(timedOutCondition.block(newTimeoutMs));
assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
// We should still be trying to open as we approach the new timeout.
assertFalse(timedOutCondition.block(50));
assertThat(timedOutCondition.block(50)).isFalse();
// A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
"RandomRedirectedUrl2");
@ -687,15 +682,15 @@ public final class CronetDataSourceTest {
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
// Give the thread some time to run.
assertFalse(timedOutCondition.block(newTimeoutMs));
assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
// We should still be trying to open as we approach the new timeout.
assertFalse(timedOutCondition.block(50));
assertThat(timedOutCondition.block(50)).isFalse();
// Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs);
timedOutCondition.block();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
assertEquals(1, openExceptions.get());
assertThat(openExceptions.get()).isEqualTo(1);
}
@Test
@ -855,7 +850,7 @@ public final class CronetDataSourceTest {
fail();
} catch (HttpDataSourceException e) {
// Expected.
assertTrue(e.getCause() instanceof CronetDataSource.InterruptedIOException);
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
timedOutCondition.open();
}
}
@ -863,7 +858,7 @@ public final class CronetDataSourceTest {
thread.start();
startCondition.block();
assertFalse(timedOutCondition.block(50));
assertThat(timedOutCondition.block(50)).isFalse();
// Now we interrupt.
thread.interrupt();
timedOutCondition.block();

View file

@ -14,7 +14,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.ext.cronet">
package="com.google.android.exoplayer2.ext.cronet">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

View file

@ -369,6 +369,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
throw new HttpDataSourceException(exception, currentDataSpec,
HttpDataSourceException.TYPE_READ);
} else if (finished) {
bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
} else {
// The operation didn't time out, fail or finish, and therefore data must have been read.

View file

@ -69,18 +69,23 @@ import java.util.List;
}
@Override
public DecoderInputBuffer createInputBuffer() {
protected DecoderInputBuffer createInputBuffer() {
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
}
@Override
public SimpleOutputBuffer createOutputBuffer() {
protected SimpleOutputBuffer createOutputBuffer() {
return new SimpleOutputBuffer(this);
}
@Override
public FfmpegDecoderException decode(DecoderInputBuffer inputBuffer,
SimpleOutputBuffer outputBuffer, boolean reset) {
protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error) {
return new FfmpegDecoderException("Unexpected decode error", error);
}
@Override
protected FfmpegDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
nativeContext = ffmpegReset(nativeContext, extraData);
if (nativeContext == 0) {

View file

@ -26,4 +26,7 @@ public final class FfmpegDecoderException extends AudioDecoderException {
super(message);
}
/* package */ FfmpegDecoderException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8880
getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:
@ -25,6 +25,7 @@ track 0:
language = null
drmInitData = -
initializationData:
total output bytes = 526272
sample count = 33
sample 0:
time = 0

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8880
getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:
@ -25,6 +25,7 @@ track 0:
language = null
drmInitData = -
initializationData:
total output bytes = 362432
sample count = 23
sample 0:
time = 853333

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8880
getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:
@ -25,6 +25,7 @@ track 0:
language = null
drmInitData = -
initializationData:
total output bytes = 182208
sample count = 12
sample 0:
time = 1792000

View file

@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
getPosition(0) = 8880
getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:
@ -25,6 +25,7 @@ track 0:
language = null
drmInitData = -
initializationData:
total output bytes = 18368
sample count = 2
sample 0:
time = 2645333

View file

@ -34,11 +34,14 @@ public class FlacExtractorTest extends InstrumentationTestCase {
}
public void testSample() throws Exception {
ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override
public Extractor create() {
return new FlacExtractor();
}
}, "bear.flac", getInstrumentation());
ExtractorAsserts.assertBehavior(
new ExtractorFactory() {
@Override
public Extractor create() {
return new FlacExtractor();
}
},
"bear.flac",
getInstrumentation().getContext());
}
}

View file

@ -70,18 +70,23 @@ import java.util.List;
}
@Override
public DecoderInputBuffer createInputBuffer() {
protected DecoderInputBuffer createInputBuffer() {
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
}
@Override
public SimpleOutputBuffer createOutputBuffer() {
protected SimpleOutputBuffer createOutputBuffer() {
return new SimpleOutputBuffer(this);
}
@Override
public FlacDecoderException decode(DecoderInputBuffer inputBuffer,
SimpleOutputBuffer outputBuffer, boolean reset) {
protected FlacDecoderException createUnexpectedDecodeException(Throwable error) {
return new FlacDecoderException("Unexpected decode error", error);
}
@Override
protected FlacDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
decoderJni.flush();
}

View file

@ -26,4 +26,7 @@ public final class FlacDecoderException extends AudioDecoderException {
super(message);
}
/* package */ FlacDecoderException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes;
@ -104,26 +105,11 @@ public final class FlacExtractor implements Extractor {
}
metadataParsed = true;
extractorOutput.seekMap(new SeekMap() {
final boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
final long durationUs = streamInfo.durationUs();
@Override
public boolean isSeekable() {
return isSeekable;
}
@Override
public long getPosition(long timeUs) {
return isSeekable ? decoderJni.getSeekPosition(timeUs) : 0;
}
@Override
public long getDurationUs() {
return durationUs;
}
});
boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
extractorOutput.seekMap(
isSeekable
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
: new SeekMap.Unseekable(streamInfo.durationUs(), 0));
Format mediaFormat =
Format.createAudioSampleFormat(
null,
@ -184,4 +170,30 @@ public final class FlacExtractor implements Extractor {
}
}
private static final class FlacSeekMap implements SeekMap {
private final long durationUs;
private final FlacDecoderJni decoderJni;
public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) {
this.durationUs = durationUs;
this.decoderJni = decoderJni;
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
// TODO: Access the seek table via JNI to return two seek points when appropriate.
return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs)));
}
@Override
public long getDurationUs() {
return durationUs;
}
}
}

View file

@ -50,7 +50,8 @@ class JavaDataSource : public DataSource {
ssize_t readAt(off64_t offset, void *const data, size_t size) {
jobject byteBuffer = env->NewDirectByteBuffer(data, size);
int result = env->CallIntMethod(flacDecoderJni, mid, byteBuffer);
if (env->ExceptionOccurred()) {
if (env->ExceptionCheck()) {
// Exception is thrown in Java when returning from the native call.
result = -1;
}
env->DeleteLocalRef(byteBuffer);

View file

@ -22,6 +22,7 @@
class DataSource {
public:
virtual ~DataSource() {}
// Returns the number of bytes read, or -1 on failure. It's not an error if
// this returns zero; it just means the given offset is equal to, or
// beyond, the end of the source.

View file

@ -0,0 +1,6 @@
# Proguard rules specific to the IMA extension.
-keep class com.google.ads.interactivemedia.** { *; }
-keep interface com.google.ads.interactivemedia.** { *; }
-keep class com.google.obf.** { *; }
-keep interface com.google.obf.** { *; }

View file

@ -25,6 +25,8 @@ import android.view.ViewGroup;
import android.webkit.WebView;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdError;
import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
import com.google.ads.interactivemedia.v3.api.AdEvent;
@ -48,6 +50,7 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
@ -160,6 +163,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/
private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000;
/** The maximum duration before an ad break that IMA may start preloading the next ad. */
private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;
/**
* The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be
* clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in
@ -211,6 +217,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Fields tracking IMA's state.
/** The expected ad group index that IMA should load next. */
private int expectedAdGroupIndex;
/**
* The index of the current ad group that IMA is loading.
*/
@ -239,9 +247,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/
private int playingAdIndexInAdGroup;
/**
* If a content period has finished but IMA has not yet sent an ad event with
* {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of
* {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to
* Whether there's a pending ad preparation error which IMA needs to be notified of when it
* transitions from playing content to playing the ad.
*/
private boolean shouldNotifyAdPrepareError;
/**
* If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value
* of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to
* determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
*/
private long fakeContentProgressElapsedRealtimeMs;
@ -332,7 +344,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*
* <p>Ads will be requested automatically when the player is prepared if this method has not been
* called, so it is only necessary to call this method if you want to request ads before preparing
* the player
* the player.
*
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
*/
@ -373,7 +385,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
} else if (contentType == C.TYPE_SS) {
// IMA does not support SmoothStreaming ad media.
// IMA does not support Smooth Streaming ad media.
}
}
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
@ -388,10 +400,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
lastContentProgress = null;
adDisplayContainer.setAdContainer(adUiViewGroup);
player.addListener(this);
maybeNotifyAdError();
maybeNotifyPendingAdLoadError();
if (adPlaybackState != null) {
// Pass the ad playback state to the player, and resume ads if necessary.
eventListener.onAdPlaybackState(adPlaybackState.copy());
eventListener.onAdPlaybackState(adPlaybackState);
if (imaPausedContent && player.getPlayWhenReady()) {
adsManager.resume();
}
@ -407,7 +419,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void detachPlayer() {
if (adsManager != null && imaPausedContent) {
adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
adPlaybackState =
adPlaybackState.withAdResumePositionUs(
playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
adsManager.pause();
}
lastAdProgress = getAdProgress();
@ -427,6 +441,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
@Override
public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) {
if (player == null) {
return;
}
try {
handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception);
} catch (Exception e) {
maybeNotifyInternalError("handlePrepareError", e);
}
}
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
@Override
@ -442,7 +468,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adsManager.addAdEventListener(this);
if (player != null) {
// If a player is attached already, start playback immediately.
startAdPlayback();
try {
startAdPlayback();
} catch (Exception e) {
maybeNotifyInternalError("onAdsManagerLoaded", e);
}
}
}
@ -451,65 +481,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void onAdEvent(AdEvent adEvent) {
AdEventType adEventType = adEvent.getType();
boolean isLogAdEvent = adEventType == AdEventType.LOG;
if (DEBUG || isLogAdEvent) {
Log.w(TAG, "onAdEvent: " + adEventType);
if (isLogAdEvent) {
for (Map.Entry<String, String> entry : adEvent.getAdData().entrySet()) {
Log.w(TAG, " " + entry.getKey() + ": " + entry.getValue());
}
}
if (DEBUG) {
Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
Log.w(TAG, "Dropping ad event after release: " + adEvent);
return;
}
Ad ad = adEvent.getAd();
switch (adEvent.getType()) {
case LOADED:
// The ad position is not always accurate when using preloading. See [Internal: b/62613240].
AdPodInfo adPodInfo = ad.getAdPodInfo();
int podIndex = adPodInfo.getPodIndex();
adGroupIndex =
podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
int adPosition = adPodInfo.getAdPosition();
int adCount = adPodInfo.getTotalAds();
adsManager.start();
if (DEBUG) {
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
}
adPlaybackState.setAdCount(adGroupIndex, adCount);
updateAdPlaybackState();
break;
case CONTENT_PAUSE_REQUESTED:
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
// before sending CONTENT_RESUME_REQUESTED.
imaPausedContent = true;
pauseContentInternal();
break;
case STARTED:
if (ad.isSkippable()) {
focusSkipButton();
}
break;
case TAPPED:
if (eventListener != null) {
eventListener.onAdTapped();
}
break;
case CLICKED:
if (eventListener != null) {
eventListener.onAdClicked();
}
break;
case CONTENT_RESUME_REQUESTED:
imaPausedContent = false;
resumeContentInternal();
break;
case ALL_ADS_COMPLETED:
// Do nothing. The ads manager will be released when the source is released.
default:
break;
try {
handleAdEvent(adEvent);
} catch (Exception e) {
maybeNotifyInternalError("onAdEvent", e);
}
}
@ -517,41 +499,68 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void onAdError(AdErrorEvent adErrorEvent) {
AdError error = adErrorEvent.getError();
if (DEBUG) {
Log.d(TAG, "onAdError " + adErrorEvent);
Log.d(TAG, "onAdError", error);
}
if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext = null;
adPlaybackState = new AdPlaybackState(new long[0]);
adPlaybackState = new AdPlaybackState();
updateAdPlaybackState();
} else if (isAdGroupLoadError(error)) {
try {
handleAdGroupLoadError();
} catch (Exception e) {
maybeNotifyInternalError("onAdError", e);
}
}
if (pendingAdErrorEvent == null) {
pendingAdErrorEvent = adErrorEvent;
}
maybeNotifyAdError();
maybeNotifyPendingAdLoadError();
}
// ContentProgressProvider implementation.
@Override
public VideoProgressUpdate getContentProgress() {
boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
if (player == null) {
return lastContentProgress;
} else if (pendingContentPositionMs != C.TIME_UNSET) {
}
boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
long contentPositionMs;
if (pendingContentPositionMs != C.TIME_UNSET) {
sentPendingContentPositionMs = true;
return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs);
contentPositionMs = pendingContentPositionMs;
expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
return new VideoProgressUpdate(fakePositionMs, contentDurationMs);
} else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
expectedAdGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
} else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
contentPositionMs = player.getCurrentPosition();
// Update the expected ad group index for the current content position. The update is delayed
// until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
// just after an ad group isn't incorrectly attributed to the next ad group.
int nextAdGroupIndex =
adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs));
if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {
long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);
if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {
nextAdGroupTimeMs = contentDurationMs;
}
if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) {
expectedAdGroupIndex = nextAdGroupIndex;
}
}
} else {
return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
return new VideoProgressUpdate(contentPositionMs, contentDurationMs);
}
// VideoAdPlayer implementation.
@ -560,22 +569,40 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
public VideoProgressUpdate getAdProgress() {
if (player == null) {
return lastAdProgress;
} else if (imaAdState == IMA_AD_STATE_NONE) {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
} else {
} else if (imaAdState != IMA_AD_STATE_NONE && playingAd) {
long adDuration = player.getDuration();
return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY
: new VideoProgressUpdate(player.getCurrentPosition(), adDuration);
} else {
return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
}
@Override
public void loadAd(String adUriString) {
if (adGroupIndex == C.INDEX_UNSET) {
Log.w(
TAG,
"Unexpected loadAd without LOADED event; assuming ad group index is actually "
+ expectedAdGroupIndex);
adGroupIndex = expectedAdGroupIndex;
adsManager.start();
}
if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
}
adPlaybackState.addAdUri(adGroupIndex, Uri.parse(adUriString));
updateAdPlaybackState();
try {
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) {
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
return;
}
adPlaybackState =
adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString));
updateAdPlaybackState();
} catch (Exception e) {
maybeNotifyInternalError("loadAd", e);
}
}
@Override
@ -600,10 +627,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.w(TAG, "Unexpected playAd without stopAd");
break;
case IMA_AD_STATE_NONE:
// IMA is requesting to play the ad, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
fakeContentProgressOffsetMs = C.TIME_UNSET;
imaAdState = IMA_AD_STATE_PLAYING;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPlay();
}
if (shouldNotifyAdPrepareError) {
shouldNotifyAdPrepareError = false;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError();
}
}
break;
case IMA_AD_STATE_PAUSED:
imaAdState = IMA_AD_STATE_PLAYING;
@ -635,7 +671,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.w(TAG, "Unexpected stopAd");
return;
}
stopAdInternal();
try {
stopAdInternal();
} catch (Exception e) {
maybeNotifyInternalError("stopAd", e);
}
}
@Override
@ -656,15 +696,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void resumeAd() {
// This method is never called. See [Internal: b/18931719].
throw new IllegalStateException();
maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd"));
}
// Player.EventListener implementation.
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
if (timeline.isEmpty()) {
// The player is being re-prepared and this source will be released.
public void onTimelineChanged(Timeline timeline, Object manifest,
@Player.TimelineChangeReason int reason) {
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
// The player is being reset and this source will be released.
return;
}
Assertions.checkArgument(timeline.getPeriodCount() == 1);
@ -672,7 +713,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
long contentDurationUs = timeline.getPeriod(0, period).durationUs;
contentDurationMs = C.usToMs(contentDurationUs);
if (contentDurationUs != C.TIME_UNSET) {
adPlaybackState.contentDurationUs = contentDurationUs;
adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
}
updateImaStateForPlayerState();
}
@ -710,7 +751,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void onPlayerError(ExoPlaybackException error) {
if (playingAd) {
if (imaAdState != IMA_AD_STATE_NONE) {
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError();
}
@ -727,16 +768,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (sentContentComplete) {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) {
adPlaybackState.playedAdGroup(i);
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
}
updateAdPlaybackState();
} else {
long positionMs = player.getCurrentPosition();
timeline.getPeriod(0, period);
if (period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)) != C.INDEX_UNSET) {
int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs));
if (newAdGroupIndex != C.INDEX_UNSET) {
sentPendingContentPositionMs = false;
pendingContentPositionMs = positionMs;
if (newAdGroupIndex != adGroupIndex) {
shouldNotifyAdPrepareError = false;
}
}
}
} else {
@ -753,21 +798,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
// Set up the ad playback state, skipping ads based on the start position as required.
pendingContentPositionMs = player.getCurrentPosition();
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
adPlaybackState = new AdPlaybackState(adGroupTimesUs);
long contentPositionMs = player.getCurrentPosition();
int adGroupIndexForPosition =
getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs));
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
if (adGroupIndexForPosition == 0) {
podIndexOffset = 0;
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
pendingContentPositionMs = C.TIME_UNSET;
// There is no preroll and midroll pod indices start at 1.
podIndexOffset = -1;
} else /* adGroupIndexForPosition > 0 */ {
// Skip ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) {
adPlaybackState.playedAdGroup(i);
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
// Play ads after the midpoint between the ad to play and the one before it, to avoid issues
// with rounding one of the two ad times.
@ -781,6 +825,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
podIndexOffset = adGroupIndexForPosition - 1;
}
if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) {
// Provide the player's initial position to trigger loading and playing the ad.
pendingContentPositionMs = contentPositionMs;
}
// Start ad playback.
adsManager.init(adsRenderingSettings);
updateAdPlaybackState();
@ -789,12 +838,76 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
private void maybeNotifyAdError() {
if (eventListener != null && pendingAdErrorEvent != null) {
IOException exception =
new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError());
eventListener.onLoadError(exception);
pendingAdErrorEvent = null;
private void handleAdEvent(AdEvent adEvent) {
Ad ad = adEvent.getAd();
switch (adEvent.getType()) {
case LOADED:
// The ad position is not always accurate when using preloading. See [Internal: b/62613240].
AdPodInfo adPodInfo = ad.getAdPodInfo();
int podIndex = adPodInfo.getPodIndex();
adGroupIndex =
podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
int adPosition = adPodInfo.getAdPosition();
int adCount = adPodInfo.getTotalAds();
adsManager.start();
if (DEBUG) {
Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
}
int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count;
if (adCount != oldAdCount) {
if (oldAdCount == C.LENGTH_UNSET) {
adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount);
updateAdPlaybackState();
} else {
// IMA sometimes unexpectedly decreases the ad count in an ad group.
Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount);
}
}
if (adGroupIndex != expectedAdGroupIndex) {
Log.w(
TAG,
"Expected ad group index "
+ expectedAdGroupIndex
+ ", actual ad group index "
+ adGroupIndex);
expectedAdGroupIndex = adGroupIndex;
}
break;
case CONTENT_PAUSE_REQUESTED:
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
// before sending CONTENT_RESUME_REQUESTED.
imaPausedContent = true;
pauseContentInternal();
break;
case STARTED:
if (ad.isSkippable()) {
focusSkipButton();
}
break;
case TAPPED:
if (eventListener != null) {
eventListener.onAdTapped();
}
break;
case CLICKED:
if (eventListener != null) {
eventListener.onAdClicked();
}
break;
case CONTENT_RESUME_REQUESTED:
imaPausedContent = false;
resumeContentInternal();
break;
case LOG:
Map<String, String> adData = adEvent.getAdData();
Log.i(TAG, "Log AdEvent: " + adData);
if ("adLoadError".equals(adData.get("type"))) {
handleAdGroupLoadError();
}
break;
case ALL_ADS_COMPLETED:
default:
break;
}
}
@ -815,9 +928,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
}
}
if (!wasPlayingAd && playingAd) {
if (!wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {
int adGroupIndex = player.getCurrentAdGroupIndex();
// IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
// IMA hasn't called playAd yet, so fake the content position.
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
@ -834,8 +947,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
}
}
if (playingAd && adGroupIndex != C.INDEX_UNSET) {
adPlaybackState.playedAdGroup(adGroupIndex);
if (adGroupIndex != C.INDEX_UNSET) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex);
adGroupIndex = C.INDEX_UNSET;
updateAdPlaybackState();
}
@ -847,21 +960,76 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
pendingContentPositionMs = C.TIME_UNSET;
sentPendingContentPositionMs = false;
}
// IMA is requesting to pause content, so stop faking the content position.
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
fakeContentProgressOffsetMs = C.TIME_UNSET;
}
private void stopAdInternal() {
Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
imaAdState = IMA_AD_STATE_NONE;
adPlaybackState.playedAd(adGroupIndex);
int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
// TODO: Handle the skipped event so the ad can be marked as skipped rather than played.
adPlaybackState =
adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0);
updateAdPlaybackState();
if (!playingAd) {
adGroupIndex = C.INDEX_UNSET;
}
}
private void handleAdGroupLoadError() {
int adGroupIndex =
this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;
if (adGroupIndex == C.INDEX_UNSET) {
// Drop the error, as we don't know which ad group it relates to.
return;
}
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
if (adGroup.count == C.LENGTH_UNSET) {
adPlaybackState =
adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length));
adGroup = adPlaybackState.adGroups[adGroupIndex];
}
for (int i = 0; i < adGroup.count; i++) {
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
if (DEBUG) {
Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
}
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
}
}
updateAdPlaybackState();
}
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
if (DEBUG) {
Log.d(
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
}
if (imaAdState == IMA_AD_STATE_NONE) {
// Send IMA a content position at the ad group so that it will try to play it, at which point
// we can notify that it failed to load.
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
fakeContentProgressOffsetMs = contentDurationMs;
}
shouldNotifyAdPrepareError = true;
} else {
// We're already playing an ad.
if (adIndexInAdGroup > playingAdIndexInAdGroup) {
// Mark the playing ad as ended so we can notify the error on the next ad and remove it,
// which means that the ad after will load (if any).
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded();
}
}
playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError();
}
}
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup);
updateAdPlaybackState();
}
private void checkForContentComplete() {
if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET
&& player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
@ -877,7 +1045,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private void updateAdPlaybackState() {
// Ignore updates while detached. When a player is attached it will receive the latest state.
if (eventListener != null) {
eventListener.onAdPlaybackState(adPlaybackState.copy());
eventListener.onAdPlaybackState(adPlaybackState);
}
}
@ -890,6 +1058,48 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
/**
* Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all
* ads in the ad group have loaded.
*/
private int getAdIndexInAdGroupToLoad(int adGroupIndex) {
@AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states;
int adIndexInAdGroup = 0;
// IMA loads ads in order.
while (adIndexInAdGroup < states.length
&& states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) {
adIndexInAdGroup++;
}
return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup;
}
private void maybeNotifyPendingAdLoadError() {
if (pendingAdErrorEvent != null) {
if (eventListener != null) {
eventListener.onAdLoadError(
new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()));
}
pendingAdErrorEvent = null;
}
}
private void maybeNotifyInternalError(String name, Exception cause) {
String message = "Internal error in " + name;
Log.e(TAG, message, cause);
if (eventListener != null) {
eventListener.onInternalAdLoadError(new RuntimeException(message, cause));
}
// We can't recover from an unexpected error in general, so skip all remaining ads.
if (adPlaybackState == null) {
adPlaybackState = new AdPlaybackState();
} else {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
}
updateAdPlaybackState();
}
private static long[] getAdGroupTimesUs(List<Float> cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.
@ -898,28 +1108,35 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
int count = cuePoints.size();
long[] adGroupTimesUs = new long[count];
int adGroupIndex = 0;
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
adGroupTimesUs[i] =
cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint);
if (cuePoint == -1.0) {
adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
} else {
adGroupTimesUs[adGroupIndex++] = (long) (C.MICROS_PER_SECOND * cuePoint);
}
}
// Cue points may be out of order, so sort them.
Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
return adGroupTimesUs;
}
/**
* Returns the index of the ad group that should be played before playing the content at {@code
* playbackPositionUs} when starting playback for the first time. This is the latest ad group at
* or before the specified playback position. If the first ad is after the playback position,
* returns {@link C#INDEX_UNSET}.
*/
private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) {
for (int i = 0; i < adGroupTimesUs.length; i++) {
long adGroupTimeUs = adGroupTimesUs[i];
// A postroll ad is after any position in the content.
if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) {
return i == 0 ? C.INDEX_UNSET : (i - 1);
}
private static boolean isAdGroupLoadError(AdError adError) {
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to
// a single ad, ad group or the whole timeline.
return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH;
}
private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) {
int count = adGroupTimesUs.length;
if (count == 1) {
return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE;
} else if (count == 2) {
return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE;
} else {
// There's at least one midroll ad group, as adGroupTimesUs is never empty.
return true;
}
return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1);
}
}

View file

@ -20,12 +20,12 @@ import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
import java.io.IOException;
/**
* A {@link MediaSource} that inserts ads linearly with a provided content media source.
@ -33,9 +33,10 @@ import java.io.IOException;
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
*/
@Deprecated
public final class ImaAdsMediaSource implements MediaSource {
public final class ImaAdsMediaSource extends CompositeMediaSource<Void> {
private final AdsMediaSource adsMediaSource;
private Listener listener;
/**
* Constructs a new source that inserts ads linearly with the content specified by
@ -74,20 +75,10 @@ public final class ImaAdsMediaSource implements MediaSource {
}
@Override
public void prepareSource(final ExoPlayer player, boolean isTopLevelSource,
final Listener listener) {
adsMediaSource.prepareSource(player, false, new Listener() {
@Override
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline,
@Nullable Object manifest) {
listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest);
}
});
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
adsMediaSource.maybeThrowSourceInfoRefreshError();
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
super.prepareSource(player, isTopLevelSource, listener);
this.listener = listener;
prepareChildSource(/* id= */ null, adsMediaSource);
}
@Override
@ -101,8 +92,8 @@ public final class ImaAdsMediaSource implements MediaSource {
}
@Override
public void releaseSource() {
adsMediaSource.releaseSource();
protected void onChildSourceInfoRefreshed(
Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {
listener.onSourceInfoRefreshed(this, timeline, manifest);
}
}

View file

@ -0,0 +1,6 @@
# Proguard rules specific to the IMA extension.
-keep class com.google.ads.interactivemedia.** { *; }
-keep interface com.google.ads.interactivemedia.** { *; }
-keep class com.google.obf.** { *; }
-keep interface com.google.obf.** { *; }

View file

@ -30,7 +30,7 @@ dependencies {
}
ext {
javadocTitle = 'Leanback extension for Exoplayer library'
javadocTitle = 'Leanback extension'
}
apply from: '../../javadoc_library.gradle'

View file

@ -30,15 +30,15 @@ import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.video.VideoListener;
/**
* Leanback {@code PlayerAdapter} implementation for {@link SimpleExoPlayer}.
*/
/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
public final class LeanbackPlayerAdapter extends PlayerAdapter {
static {
@ -46,11 +46,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
private final Context context;
private final SimpleExoPlayer player;
private final Player player;
private final Handler handler;
private final ComponentListener componentListener;
private final Runnable updateProgressRunnable;
private @Nullable PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private SurfaceHolderGlueHost surfaceHolderGlueHost;
@ -59,14 +60,14 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
/**
* Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the
* {@link SimpleExoPlayer} instance. The caller remains responsible for releasing the player when
* it's no longer required.
* {@link Player} instance. The caller remains responsible for releasing the player when it's no
* longer required.
*
* @param context The current context (activity).
* @param player Instance of your exoplayer that needs to be configured.
* @param updatePeriodMs The delay between player control updates, in milliseconds.
*/
public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, final int updatePeriodMs) {
public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {
this.context = context;
this.player = player;
handler = new Handler();
@ -83,6 +84,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
};
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}.
*/
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
this.playbackPreparer = playbackPreparer;
}
/**
* Sets the {@link ControlDispatcher}.
*
@ -114,13 +124,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
notifyStateChanged();
player.addListener(componentListener);
player.addVideoListener(componentListener);
Player.VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
videoComponent.addVideoListener(componentListener);
}
}
@Override
public void onDetachedFromHost() {
player.removeListener(componentListener);
player.removeVideoListener(componentListener);
Player.VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
videoComponent.removeVideoListener(componentListener);
}
if (surfaceHolderGlueHost != null) {
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
surfaceHolderGlueHost = null;
@ -160,7 +176,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
@Override
public void play() {
if (player.getPlaybackState() == Player.STATE_ENDED) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
if (controlDispatcher.dispatchSetPlayWhenReady(player, true)) {
@ -195,7 +215,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
/* package */ void setVideoSurface(Surface surface) {
hasSurface = surface != null;
player.setVideoSurface(surface);
Player.VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
videoComponent.setVideoSurface(surface);
}
maybeNotifyPreparedStateChanged(getCallback());
}
@ -218,8 +241,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
}
private final class ComponentListener extends Player.DefaultEventListener implements
SimpleExoPlayer.VideoListener, SurfaceHolder.Callback {
private final class ComponentListener extends Player.DefaultEventListener
implements SurfaceHolder.Callback, VideoListener {
// SurfaceHolder.Callback implementation.
@ -258,7 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
public void onTimelineChanged(Timeline timeline, Object manifest,
@TimelineChangeReason int reason) {
Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
@ -272,11 +296,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
}
// SimpleExoplayerView.Callback implementation.
// VideoListener implementation.
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height);
}

View file

@ -655,7 +655,8 @@ public final class MediaSessionConnector {
private int currentWindowCount;
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
public void onTimelineChanged(Timeline timeline, Object manifest,
@Player.TimelineChangeReason int reason) {
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Herhaal alles"</string>
<string name="exo_media_action_repeat_off_description">"Herhaal niks"</string>
<string name="exo_media_action_repeat_one_description">"Herhaal een"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Herhaal niks"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Herhaal een"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Herhaal alles"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"ሁሉንም ድገም"</string>
<string name="exo_media_action_repeat_off_description">"ምንም አትድገም"</string>
<string name="exo_media_action_repeat_one_description">"አንዱን ድገም"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"ምንም አትድገም"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"አንድ ድገም"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"ሁሉንም ድገም"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"تكرار الكل"</string>
<string name="exo_media_action_repeat_off_description">"عدم التكرار"</string>
<string name="exo_media_action_repeat_one_description">"تكرار مقطع واحد"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"عدم التكرار"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"تكرار مقطع صوتي واحد"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"تكرار الكل"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ponovi sve"</string>
<string name="exo_media_action_repeat_off_description">"Ne ponavljaj nijednu"</string>
<string name="exo_media_action_repeat_one_description">"Ponovi jednu"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ne ponavljaj nijednu"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Ponovi jednu"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Ponovi sve"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Повтаряне на всички"</string>
<string name="exo_media_action_repeat_off_description">"Без повтаряне"</string>
<string name="exo_media_action_repeat_one_description">"Повтаряне на един елемент"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Без повтаряне"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Повтаряне на един елемент"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Повтаряне на всички"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeteix-ho tot"</string>
<string name="exo_media_action_repeat_off_description">"No en repeteixis cap"</string>
<string name="exo_media_action_repeat_one_description">"Repeteix-ne un"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"No en repeteixis cap"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeteix una"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeteix tot"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Opakovat vše"</string>
<string name="exo_media_action_repeat_off_description">"Neopakovat"</string>
<string name="exo_media_action_repeat_one_description">"Opakovat jednu položku"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Neopakovat"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Opakovat jednu"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Opakovat vše"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Gentag alle"</string>
<string name="exo_media_action_repeat_off_description">"Gentag ingen"</string>
<string name="exo_media_action_repeat_one_description">"Gentag en"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Gentag ingen"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Gentag én"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Gentag alle"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Alle wiederholen"</string>
<string name="exo_media_action_repeat_off_description">"Keinen Titel wiederholen"</string>
<string name="exo_media_action_repeat_one_description">"Einen Titel wiederholen"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Keinen wiederholen"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Einen wiederholen"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Alle wiederholen"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Επανάληψη όλων"</string>
<string name="exo_media_action_repeat_off_description">"Καμία επανάληψη"</string>
<string name="exo_media_action_repeat_one_description">"Επανάληψη ενός στοιχείου"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Καμία επανάληψη"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Επανάληψη ενός κομματιού"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Επανάληψη όλων"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeat one"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeat all"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeat one"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeat all"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repeat one"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repeat all"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
<string name="exo_media_action_repeat_off_description">"No repetir"</string>
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"No repetir"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repetir uno"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repetir todo"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
<string name="exo_media_action_repeat_off_description">"No repetir"</string>
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"No repetir"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Repetir uno"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Repetir todo"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"تکرار همه"</string>
<string name="exo_media_action_repeat_off_description">"تکرار هیچ‌کدام"</string>
<string name="exo_media_action_repeat_one_description">"یک‌بار تکرار"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"تکرار هیچ‌کدام"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"یکبار تکرار"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"تکرار همه"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Toista kaikki"</string>
<string name="exo_media_action_repeat_off_description">"Toista ei mitään"</string>
<string name="exo_media_action_repeat_one_description">"Toista yksi"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ei uudelleentoistoa"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Toista yksi uudelleen"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Toista kaikki uudelleen"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Tout lire en boucle"</string>
<string name="exo_media_action_repeat_off_description">"Aucune répétition"</string>
<string name="exo_media_action_repeat_one_description">"Répéter un élément"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ne rien lire en boucle"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Lire une chanson en boucle"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Tout lire en boucle"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Tout lire en boucle"</string>
<string name="exo_media_action_repeat_off_description">"Ne rien lire en boucle"</string>
<string name="exo_media_action_repeat_one_description">"Lire en boucle un élément"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ne rien lire en boucle"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Lire un titre en boucle"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Tout lire en boucle"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"सभी को दोहराएं"</string>
<string name="exo_media_action_repeat_off_description">"कुछ भी न दोहराएं"</string>
<string name="exo_media_action_repeat_one_description">"एक दोहराएं"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"किसी को न दोहराएं"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"एक को दोहराएं"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"सभी को दोहराएं"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ponovi sve"</string>
<string name="exo_media_action_repeat_off_description">"Bez ponavljanja"</string>
<string name="exo_media_action_repeat_one_description">"Ponovi jedno"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Bez ponavljanja"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Ponovi jedno"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Ponovi sve"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Összes ismétlése"</string>
<string name="exo_media_action_repeat_off_description">"Nincs ismétlés"</string>
<string name="exo_media_action_repeat_one_description">"Egy ismétlése"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Nincs ismétlés"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Egy szám ismétlése"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Összes szám ismétlése"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ulangi Semua"</string>
<string name="exo_media_action_repeat_off_description">"Jangan Ulangi"</string>
<string name="exo_media_action_repeat_one_description">"Ulangi Satu"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Jangan ulangi"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Ulangi 1"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Ulangi semua"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ripeti tutti"</string>
<string name="exo_media_action_repeat_off_description">"Non ripetere nessuno"</string>
<string name="exo_media_action_repeat_one_description">"Ripeti uno"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Non ripetere nulla"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Ripeti uno"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Ripeti tutto"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"חזור על הכל"</string>
<string name="exo_media_action_repeat_off_description">"אל תחזור על כלום"</string>
<string name="exo_media_action_repeat_one_description">"חזור על פריט אחד"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"אל תחזור על אף פריט"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"חזור על פריט אחד"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"חזור על הכול"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"全曲を繰り返し"</string>
<string name="exo_media_action_repeat_off_description">"繰り返しなし"</string>
<string name="exo_media_action_repeat_one_description">"1曲を繰り返し"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"リピートなし"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"1 曲をリピート"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"全曲をリピート"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"전체 반복"</string>
<string name="exo_media_action_repeat_off_description">"반복 안함"</string>
<string name="exo_media_action_repeat_one_description">"한 항목 반복"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"반복 안함"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"현재 미디어 반복"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"모두 반복"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Kartoti viską"</string>
<string name="exo_media_action_repeat_off_description">"Nekartoti nieko"</string>
<string name="exo_media_action_repeat_one_description">"Kartoti vieną"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Nekartoti nieko"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Kartoti vieną"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Kartoti viską"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Atkārtot visu"</string>
<string name="exo_media_action_repeat_off_description">"Neatkārtot nevienu"</string>
<string name="exo_media_action_repeat_one_description">"Atkārtot vienu"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Neatkārtot nevienu"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Atkārtot vienu"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Atkārtot visu"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Gjenta alle"</string>
<string name="exo_media_action_repeat_off_description">"Ikke gjenta noen"</string>
<string name="exo_media_action_repeat_one_description">"Gjenta én"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Ikke gjenta noen"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Gjenta én"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Gjenta alle"</string>
</resources>

View file

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
<?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.
@ -13,9 +12,11 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Alles herhalen"</string>
<string name="exo_media_action_repeat_off_description">"Niet herhalen"</string>
<string name="exo_media_action_repeat_one_description">"Eén herhalen"</string>
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_media_action_repeat_off_description" msgid="160802313171921598">"Niets herhalen"</string>
<string name="exo_media_action_repeat_one_description" msgid="120730756187958757">"Eén herhalen"</string>
<string name="exo_media_action_repeat_all_description" msgid="92377890871273452">"Alles herhalen"</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more