Commit 62092e66 by Oliver Woodman Committed by GitHub

Merge pull request #6888 from google/dev-v2-r2.11.2

r2.11.2
parents ea342a67 0740e3cc
Showing with 2532 additions and 712 deletions
...@@ -58,6 +58,7 @@ extensions/vp9/src/main/jni/libvpx_android_configs ...@@ -58,6 +58,7 @@ extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv extensions/vp9/src/main/jni/libyuv
# AV1 extension # AV1 extension
extensions/av1/src/main/jni/cpu_features
extensions/av1/src/main/jni/libgav1 extensions/av1/src/main/jni/libgav1
# Opus extension # Opus extension
......
# Release notes # # Release notes #
### 2.11.2 (2020-02-13) ###
* Add Java FLAC extractor
([#6406](https://github.com/google/ExoPlayer/issues/6406)).
* Startup latency optimization:
* Reduce startup latency for DASH and SmoothStreaming playbacks by allowing
codec initialization to occur before the network connection for the first
media segment has been established.
* Reduce startup latency for on-demand DASH playbacks by allowing codec
initialization to occur before the sidx box has been loaded.
* Downloads:
* Fix download resumption when the requirements for them to continue are
met ([#6733](https://github.com/google/ExoPlayer/issues/6733),
[#6798](https://github.com/google/ExoPlayer/issues/6798)).
* Fix `DownloadHelper.createMediaSource` to use `customCacheKey` when creating
`ProgressiveMediaSource` instances.
* DRM: Fix `NullPointerException` when playing DRM-protected content
([#6951](https://github.com/google/ExoPlayer/issues/6951)).
* Metadata:
* Update `IcyDecoder` to try ISO-8859-1 decoding if UTF-8 decoding fails.
Also change `IcyInfo.rawMetadata` from `String` to `byte[]` to allow
developers to handle data that's neither UTF-8 nor ISO-8859-1
([#6753](https://github.com/google/ExoPlayer/issues/6753)).
* Select multiple metadata tracks if multiple metadata renderers are available
([#6676](https://github.com/google/ExoPlayer/issues/6676)).
* Add support for ID3 genres added in Wimamp 5.6 (2010).
* UI:
* Show ad group markers in `DefaultTimeBar` even if they are after the end
of the current window
([#6552](https://github.com/google/ExoPlayer/issues/6552)).
* Don't use notification chronometer if playback speed is != 1.0
([#6816](https://github.com/google/ExoPlayer/issues/6816)).
* HLS: Fix playback of DRM protected content that uses key rotation
([#6903](https://github.com/google/ExoPlayer/issues/6903)).
* WAV:
* Support IMA ADPCM encoded data.
* Improve support for G.711 A-law and mu-law encoded data.
* MP4: Support "twos" codec (big endian PCM)
([#5789](https://github.com/google/ExoPlayer/issues/5789)).
* FMP4: Add support for encrypted AC-4 tracks.
* HLS: Fix slow seeking into long MP3 segments
([#6155](https://github.com/google/ExoPlayer/issues/6155)).
* Fix handling of E-AC-3 streams that contain AC-3 syncframes
([#6602](https://github.com/google/ExoPlayer/issues/6602)).
* Fix playback of TrueHD streams in Matroska
([#6845](https://github.com/google/ExoPlayer/issues/6845)).
* Fix MKV subtitles to disappear when intended instead of lasting until the
next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)).
* OkHttp extension: Upgrade OkHttp dependency to 3.12.8, which fixes a class of
`SocketTimeoutException` issues when using HTTP/2
([#4078](https://github.com/google/ExoPlayer/issues/4078)).
* FLAC extension: Fix handling of bit depths other than 16 in `FLACDecoder`.
This issue caused FLAC streams with other bit depths to sound like white noise
on earlier releases, but only when embedded in a non-FLAC container such as
Matroska or MP4.
* Demo apps: Add
[GL demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/gl) to
show how to render video to a `GLSurfaceView` while applying a GL shader.
([#6920](https://github.com/google/ExoPlayer/issues/6920)).
### 2.11.1 (2019-12-20) ### ### 2.11.1 (2019-12-20) ###
* UI: Exclude `DefaultTimeBar` region from system gesture detection * UI: Exclude `DefaultTimeBar` region from system gesture detection
...@@ -165,7 +225,7 @@ ...@@ -165,7 +225,7 @@
`C.MSG_SET_OUTPUT_BUFFER_RENDERER`. `C.MSG_SET_OUTPUT_BUFFER_RENDERER`.
* Use `VideoDecoderRenderer` as an implementation of * Use `VideoDecoderRenderer` as an implementation of
`VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`. `VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`.
* Flac extension: Update to use NDK r20. * FLAC extension: Update to use NDK r20.
* Opus extension: Update to use NDK r20. * Opus extension: Update to use NDK r20.
* FFmpeg extension: * FFmpeg extension:
* Update to use NDK r20. * Update to use NDK r20.
...@@ -302,7 +362,7 @@ ...@@ -302,7 +362,7 @@
([#6241](https://github.com/google/ExoPlayer/issues/6241)). ([#6241](https://github.com/google/ExoPlayer/issues/6241)).
* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change * MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change
from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)). from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)).
* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata * FLAC extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata
([#5527](https://github.com/google/ExoPlayer/issues/5527)). ([#5527](https://github.com/google/ExoPlayer/issues/5527)).
* Fix issue where initial seek positions get ignored when playing a preroll ad * Fix issue where initial seek positions get ignored when playing a preroll ad
([#6201](https://github.com/google/ExoPlayer/issues/6201)). ([#6201](https://github.com/google/ExoPlayer/issues/6201)).
...@@ -311,7 +371,7 @@ ...@@ -311,7 +371,7 @@
([#6153](https://github.com/google/ExoPlayer/issues/6153)). ([#6153](https://github.com/google/ExoPlayer/issues/6153)).
* Fix `DataSchemeDataSource` re-opening and range requests * Fix `DataSchemeDataSource` re-opening and range requests
([#6192](https://github.com/google/ExoPlayer/issues/6192)). ([#6192](https://github.com/google/ExoPlayer/issues/6192)).
* Fix Flac and ALAC playback on some LG devices * Fix FLAC and ALAC playback on some LG devices
([#5938](https://github.com/google/ExoPlayer/issues/5938)). ([#5938](https://github.com/google/ExoPlayer/issues/5938)).
* Fix issue when calling `performClick` on `PlayerView` without * Fix issue when calling `performClick` on `PlayerView` without
`PlayerControlView` `PlayerControlView`
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.11.1' releaseVersion = '2.11.2'
releaseVersionCode = 2011001 releaseVersionCode = 2011002
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 29 appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
......
# ExoPlayer GL demo
This app demonstrates how to render video to a [GLSurfaceView][] while applying
a GL shader.
The shader shows an overlap bitmap on top of the video. The overlay bitmap is
drawn using an Android canvas, and includes the current frame's presentation
timestamp, to show how to get the timestamp of the frame currently in the
off-screen surface texture.
[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
}
lintOptions {
// This demo app does not have translations.
disable 'MissingTranslation'
}
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'library-dash')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.gldemo">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-sdk/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="com.google.android.exoplayer.gldemo.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
</activity>
</application>
</manifest>
// Copyright 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#extension GL_OES_EGL_image_external : require
precision mediump float;
// External texture containing video decoder output.
uniform samplerExternalOES tex_sampler_0;
// Texture containing the overlap bitmap.
uniform sampler2D tex_sampler_1;
// Horizontal scaling factor for the overlap bitmap.
uniform float scaleX;
// Vertical scaling factory for the overlap bitmap.
uniform float scaleY;
varying vec2 v_texcoord;
void main() {
vec4 videoColor = texture2D(tex_sampler_0, v_texcoord);
vec4 overlayColor = texture2D(tex_sampler_1,
vec2(v_texcoord.x * scaleX,
v_texcoord.y * scaleY));
// Blend the video decoder output and the overlay bitmap.
gl_FragColor = videoColor * (1.0 - overlayColor.a)
+ overlayColor * overlayColor.a;
}
// Copyright 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
attribute vec4 a_position;
attribute vec3 a_texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = a_position;
v_texcoord = a_texcoord.xy;
}
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.gldemo;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.GlUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import javax.microedition.khronos.opengles.GL10;
/**
* Video processor that demonstrates how to overlay a bitmap on video output using a GL shader. The
* bitmap is drawn using an Android {@link Canvas}.
*/
/* package */ final class BitmapOverlayVideoProcessor
implements VideoProcessingGLSurfaceView.VideoProcessor {
private static final int OVERLAY_WIDTH = 512;
private static final int OVERLAY_HEIGHT = 256;
private final Context context;
private final Paint paint;
private final int[] textures;
private final Bitmap overlayBitmap;
private final Bitmap logoBitmap;
private final Canvas overlayCanvas;
private int program;
@Nullable private GlUtil.Attribute[] attributes;
@Nullable private GlUtil.Uniform[] uniforms;
private float bitmapScaleX;
private float bitmapScaleY;
public BitmapOverlayVideoProcessor(Context context) {
this.context = context.getApplicationContext();
paint = new Paint();
paint.setTextSize(64);
paint.setAntiAlias(true);
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
textures = new int[1];
overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888);
overlayCanvas = new Canvas(overlayBitmap);
try {
logoBitmap =
((BitmapDrawable)
context.getPackageManager().getApplicationIcon(context.getPackageName()))
.getBitmap();
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e);
}
}
@Override
public void initialize() {
String vertexShaderCode =
loadAssetAsString(context, "bitmap_overlay_video_processor_vertex.glsl");
String fragmentShaderCode =
loadAssetAsString(context, "bitmap_overlay_video_processor_fragment.glsl");
program = GlUtil.compileProgram(vertexShaderCode, fragmentShaderCode);
GlUtil.Attribute[] attributes = GlUtil.getAttributes(program);
GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program);
for (GlUtil.Attribute attribute : attributes) {
if (attribute.name.equals("a_position")) {
attribute.setBuffer(
new float[] {
-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f,
},
4);
} else if (attribute.name.equals("a_texcoord")) {
attribute.setBuffer(
new float[] {
0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,
},
3);
}
}
this.attributes = attributes;
this.uniforms = uniforms;
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
}
@Override
public void setSurfaceSize(int width, int height) {
bitmapScaleX = (float) width / OVERLAY_WIDTH;
bitmapScaleY = (float) height / OVERLAY_HEIGHT;
}
@Override
public void draw(int frameTexture, long frameTimestampUs) {
// Draw to the canvas and store it in a texture.
String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND);
overlayBitmap.eraseColor(Color.TRANSPARENT);
overlayCanvas.drawBitmap(logoBitmap, /* left= */ 32, /* top= */ 32, paint);
overlayCanvas.drawText(text, /* x= */ 200, /* y= */ 130, paint);
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLUtils.texSubImage2D(
GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap);
GlUtil.checkGlError();
// Run the shader program.
GlUtil.Uniform[] uniforms = Assertions.checkNotNull(this.uniforms);
GlUtil.Attribute[] attributes = Assertions.checkNotNull(this.attributes);
GLES20.glUseProgram(program);
for (GlUtil.Uniform uniform : uniforms) {
switch (uniform.name) {
case "tex_sampler_0":
uniform.setSamplerTexId(frameTexture, /* unit= */ 0);
break;
case "tex_sampler_1":
uniform.setSamplerTexId(textures[0], /* unit= */ 1);
break;
case "scaleX":
uniform.setFloat(bitmapScaleX);
break;
case "scaleY":
uniform.setFloat(bitmapScaleY);
break;
}
}
for (GlUtil.Attribute copyExternalAttribute : attributes) {
copyExternalAttribute.bind();
}
for (GlUtil.Uniform copyExternalUniform : uniforms) {
copyExternalUniform.bind();
}
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
}
private static String loadAssetAsString(Context context, String assetFileName) {
@Nullable InputStream inputStream = null;
try {
inputStream = context.getAssets().open(assetFileName);
return Util.fromUtf8Bytes(Util.toByteArray(inputStream));
} catch (IOException e) {
throw new IllegalStateException(e);
} finally {
Util.closeQuietly(inputStream);
}
}
}
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.gldemo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.GlUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.UUID;
/**
* Activity that demonstrates playback of video to an {@link android.opengl.GLSurfaceView} with
* postprocessing of the video content using GL.
*/
public final class MainActivity extends Activity {
private static final String DEFAULT_MEDIA_URI =
"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv";
private static final String ACTION_VIEW = "com.google.android.exoplayer.gldemo.action.VIEW";
private static final String EXTENSION_EXTRA = "extension";
private static final String DRM_SCHEME_EXTRA = "drm_scheme";
private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
@Nullable private PlayerView playerView;
@Nullable private VideoProcessingGLSurfaceView videoProcessingGLSurfaceView;
@Nullable private SimpleExoPlayer player;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
playerView = findViewById(R.id.player_view);
Context context = getApplicationContext();
boolean requestSecureSurface = getIntent().hasExtra(DRM_SCHEME_EXTRA);
if (requestSecureSurface && !GlUtil.isProtectedContentExtensionSupported(context)) {
Toast.makeText(
context, R.string.error_protected_content_extension_not_supported, Toast.LENGTH_LONG)
.show();
}
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
new VideoProcessingGLSurfaceView(
context, requestSecureSurface, new BitmapOverlayVideoProcessor(context));
FrameLayout contentFrame = findViewById(R.id.exo_content_frame);
contentFrame.addView(videoProcessingGLSurfaceView);
this.videoProcessingGLSurfaceView = videoProcessingGLSurfaceView;
}
@Override
public void onStart() {
super.onStart();
if (Util.SDK_INT > 23) {
initializePlayer();
if (playerView != null) {
playerView.onResume();
}
}
}
@Override
public void onResume() {
super.onResume();
if (Util.SDK_INT <= 23 || player == null) {
initializePlayer();
if (playerView != null) {
playerView.onResume();
}
}
}
@Override
public void onPause() {
super.onPause();
if (Util.SDK_INT <= 23) {
if (playerView != null) {
playerView.onPause();
}
releasePlayer();
}
}
@Override
public void onStop() {
super.onStop();
if (Util.SDK_INT > 23) {
if (playerView != null) {
playerView.onPause();
}
releasePlayer();
}
}
private void initializePlayer() {
Intent intent = getIntent();
String action = intent.getAction();
Uri uri =
ACTION_VIEW.equals(action)
? Assertions.checkNotNull(intent.getData())
: Uri.parse(DEFAULT_MEDIA_URI);
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
DrmSessionManager<ExoMediaCrypto> drmSessionManager;
if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback);
} else {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
}
DataSource.Factory dataSourceFactory =
new DefaultDataSourceFactory(
this, Util.getUserAgent(this, getString(R.string.application_name)));
MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
} else if (type == C.TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
} else {
throw new IllegalStateException();
}
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
player.setRepeatMode(Player.REPEAT_MODE_ALL);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
Assertions.checkNotNull(this.videoProcessingGLSurfaceView);
videoProcessingGLSurfaceView.setVideoComponent(
Assertions.checkNotNull(player.getVideoComponent()));
Assertions.checkNotNull(playerView).setPlayer(player);
player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null));
this.player = player;
}
private void releasePlayer() {
Assertions.checkNotNull(playerView).setPlayer(null);
if (player != null) {
player.release();
Assertions.checkNotNull(videoProcessingGLSurfaceView).setVideoComponent(null);
player = null;
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:surface_type="none"/>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="application_name">ExoPlayer GL demo</string>
<string name="error_protected_content_extension_not_supported">The GL protected content extension is not supported.</string>
</resources>
...@@ -412,6 +412,10 @@ ...@@ -412,6 +412,10 @@
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
}, },
{ {
"name": "Google Play (FLAC)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac"
},
{
"name": "Big Buck Bunny video (FLV)", "name": "Big Buck Bunny video (FLV)",
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
}, },
...@@ -596,6 +600,13 @@ ...@@ -596,6 +600,13 @@
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml", "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
"subtitle_mime_type": "application/ttml+xml", "subtitle_mime_type": "application/ttml+xml",
"subtitle_language": "en" "subtitle_language": "en"
},
{
"name": "SSA/ASS position & alignment",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass",
"subtitle_mime_type": "text/x-ssa",
"subtitle_language": "en"
} }
] ]
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Notification; import android.app.Notification;
import android.content.Context;
import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
...@@ -32,8 +33,6 @@ public class DemoDownloadService extends DownloadService { ...@@ -32,8 +33,6 @@ public class DemoDownloadService extends DownloadService {
private static final int JOB_ID = 1; private static final int JOB_ID = 1;
private static final int FOREGROUND_NOTIFICATION_ID = 1; private static final int FOREGROUND_NOTIFICATION_ID = 1;
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
private DownloadNotificationHelper notificationHelper; private DownloadNotificationHelper notificationHelper;
public DemoDownloadService() { public DemoDownloadService() {
...@@ -43,7 +42,6 @@ public class DemoDownloadService extends DownloadService { ...@@ -43,7 +42,6 @@ public class DemoDownloadService extends DownloadService {
CHANNEL_ID, CHANNEL_ID,
R.string.exo_download_notification_channel_name, R.string.exo_download_notification_channel_name,
/* channelDescriptionResourceId= */ 0); /* channelDescriptionResourceId= */ 0);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
} }
@Override @Override
...@@ -54,7 +52,13 @@ public class DemoDownloadService extends DownloadService { ...@@ -54,7 +52,13 @@ public class DemoDownloadService extends DownloadService {
@Override @Override
protected DownloadManager getDownloadManager() { protected DownloadManager getDownloadManager() {
return ((DemoApplication) getApplication()).getDownloadManager(); DownloadManager downloadManager = ((DemoApplication) getApplication()).getDownloadManager();
// This will only happen once, because getDownloadManager is guaranteed to be called only once
// in the life cycle of the process.
downloadManager.addListener(
new TerminalStateNotificationHelper(
this, notificationHelper, FOREGROUND_NOTIFICATION_ID + 1));
return downloadManager;
} }
@Override @Override
...@@ -68,8 +72,28 @@ public class DemoDownloadService extends DownloadService { ...@@ -68,8 +72,28 @@ public class DemoDownloadService extends DownloadService {
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads); R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
} }
/**
* Creates and displays notifications for downloads when they complete or fail.
*
* <p>This helper will outlive the lifespan of a single instance of {@link DemoDownloadService}.
* It is static to avoid leaking the first {@link DemoDownloadService} instance.
*/
private static final class TerminalStateNotificationHelper implements DownloadManager.Listener {
private final Context context;
private final DownloadNotificationHelper notificationHelper;
private int nextNotificationId;
public TerminalStateNotificationHelper(
Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) {
this.context = context.getApplicationContext();
this.notificationHelper = notificationHelper;
nextNotificationId = firstNotificationId;
}
@Override @Override
protected void onDownloadChanged(Download download) { public void onDownloadChanged(DownloadManager manager, Download download) {
Notification notification; Notification notification;
if (download.state == Download.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
notification = notification =
...@@ -86,6 +110,7 @@ public class DemoDownloadService extends DownloadService { ...@@ -86,6 +110,7 @@ public class DemoDownloadService extends DownloadService {
} else { } else {
return; return;
} }
NotificationUtil.setNotification(this, nextNotificationId++, notification); NotificationUtil.setNotification(context, nextNotificationId++, notification);
}
} }
} }
...@@ -432,6 +432,11 @@ public class PlayerActivity extends AppCompatActivity ...@@ -432,6 +432,11 @@ public class PlayerActivity extends AppCompatActivity
mediaSources[i] = createLeafMediaSource(samples[i]); mediaSources[i] = createLeafMediaSource(samples[i]);
Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo; Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo;
if (subtitleInfo != null) { if (subtitleInfo != null) {
if (Util.maybeRequestReadExternalStoragePermission(
/* activity= */ this, subtitleInfo.uri)) {
// The player will be reinitialized if the permission is granted.
return null;
}
Format subtitleFormat = Format subtitleFormat =
Format.createTextSampleFormat( Format.createTextSampleFormat(
/* id= */ null, /* id= */ null,
...@@ -507,7 +512,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -507,7 +512,7 @@ public class PlayerActivity extends AppCompatActivity
} }
private MediaSource createLeafMediaSource( private MediaSource createLeafMediaSource(
Uri uri, String extension, DrmSessionManager<ExoMediaCrypto> drmSessionManager) { Uri uri, String extension, DrmSessionManager<?> drmSessionManager) {
@ContentType int type = Util.inferContentType(uri, extension); @ContentType int type = Util.inferContentType(uri, extension);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
...@@ -615,10 +620,20 @@ public class PlayerActivity extends AppCompatActivity ...@@ -615,10 +620,20 @@ public class PlayerActivity extends AppCompatActivity
} }
MediaSourceFactory adMediaSourceFactory = MediaSourceFactory adMediaSourceFactory =
new MediaSourceFactory() { new MediaSourceFactory() {
private DrmSessionManager<?> drmSessionManager =
DrmSessionManager.getDummyDrmSessionManager();
@Override
public MediaSourceFactory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
this.drmSessionManager = drmSessionManager;
return this;
}
@Override @Override
public MediaSource createMediaSource(Uri uri) { public MediaSource createMediaSource(Uri uri) {
return PlayerActivity.this.createLeafMediaSource( return PlayerActivity.this.createLeafMediaSource(
uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager()); uri, /* extension=*/ null, drmSessionManager);
} }
@Override @Override
......
...@@ -11,9 +11,15 @@ project(libgav1JNI C CXX) ...@@ -11,9 +11,15 @@ project(libgav1JNI C CXX)
# armeabi-v7a build. This flag enables it. # armeabi-v7a build. This flag enables it.
if(${ANDROID_ABI} MATCHES "armeabi-v7a") if(${ANDROID_ABI} MATCHES "armeabi-v7a")
add_compile_options("-mfpu=neon") add_compile_options("-mfpu=neon")
add_compile_options("-marm")
add_compile_options("-fPIC") add_compile_options("-fPIC")
endif() endif()
string(TOLOWER "${CMAKE_BUILD_TYPE}" build_type)
if(build_type MATCHES "^rel")
add_compile_options("-O2")
endif()
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}") set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
set(libgav1_jni_build "${CMAKE_BINARY_DIR}") set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
set(libgav1_jni_output_directory set(libgav1_jni_output_directory
......
...@@ -98,8 +98,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -98,8 +98,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
Assertions.checkNotNull(format.sampleMimeType); Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) { if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding) } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType) || !isOutputSupported(format)) {
|| !isOutputSupported(format)) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
......
...@@ -64,9 +64,7 @@ import java.util.List; ...@@ -64,9 +64,7 @@ import java.util.List;
throw new FfmpegDecoderException("Failed to load decoder native libraries."); throw new FfmpegDecoderException("Failed to load decoder native libraries.");
} }
Assertions.checkNotNull(format.sampleMimeType); Assertions.checkNotNull(format.sampleMimeType);
codecName = codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType));
Assertions.checkNotNull(
FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));
extraData = getExtraData(format.sampleMimeType, format.initializationData); extraData = getExtraData(format.sampleMimeType, format.initializationData);
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
...@@ -145,16 +143,12 @@ import java.util.List; ...@@ -145,16 +143,12 @@ import java.util.List;
nativeContext = 0; nativeContext = 0;
} }
/** /** Returns the channel count of output audio. */
* Returns the channel count of output audio. May only be called after {@link #decode}.
*/
public int getChannelCount() { public int getChannelCount() {
return channelCount; return channelCount;
} }
/** /** Returns the sample rate of output audio. */
* Returns the sample rate of output audio. May only be called after {@link #decode}.
*/
public int getSampleRate() { public int getSampleRate() {
return sampleRate; return sampleRate;
} }
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
...@@ -65,13 +64,12 @@ public final class FfmpegLibrary { ...@@ -65,13 +64,12 @@ public final class FfmpegLibrary {
* Returns whether the underlying library supports the specified MIME type. * Returns whether the underlying library supports the specified MIME type.
* *
* @param mimeType The MIME type to check. * @param mimeType The MIME type to check.
* @param encoding The PCM encoding for raw audio.
*/ */
public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) { public static boolean supportsFormat(String mimeType) {
if (!isAvailable()) { if (!isAvailable()) {
return false; return false;
} }
String codecName = getCodecName(mimeType, encoding); String codecName = getCodecName(mimeType);
if (codecName == null) { if (codecName == null) {
return false; return false;
} }
...@@ -86,7 +84,7 @@ public final class FfmpegLibrary { ...@@ -86,7 +84,7 @@ public final class FfmpegLibrary {
* Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null} * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null}
* if it's unsupported. * if it's unsupported.
*/ */
/* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) { /* package */ static @Nullable String getCodecName(String mimeType) {
switch (mimeType) { switch (mimeType) {
case MimeTypes.AUDIO_AAC: case MimeTypes.AUDIO_AAC:
return "aac"; return "aac";
...@@ -116,14 +114,10 @@ public final class FfmpegLibrary { ...@@ -116,14 +114,10 @@ public final class FfmpegLibrary {
return "flac"; return "flac";
case MimeTypes.AUDIO_ALAC: case MimeTypes.AUDIO_ALAC:
return "alac"; return "alac";
case MimeTypes.AUDIO_RAW: case MimeTypes.AUDIO_MLAW:
if (encoding == C.ENCODING_PCM_MU_LAW) {
return "pcm_mulaw"; return "pcm_mulaw";
} else if (encoding == C.ENCODING_PCM_A_LAW) { case MimeTypes.AUDIO_ALAW:
return "pcm_alaw"; return "pcm_alaw";
} else {
return null;
}
default: default:
return null; return null;
} }
......
config:
encoding = 2 (16 bit)
channel count = 2
sample rate = 48000
buffer:
time = 1000
data = 1217833679
buffer:
time = 97000
data = 558614672
buffer:
time = 193000
data = -709714787
buffer:
time = 289000
data = 1367870571
buffer:
time = 385000
data = -141229457
buffer:
time = 481000
data = 1287758361
buffer:
time = 577000
data = 1125289147
buffer:
time = 673000
data = -1677383475
buffer:
time = 769000
data = 2130742861
buffer:
time = 865000
data = -1292320253
buffer:
time = 961000
data = -456587163
buffer:
time = 1057000
data = 748981534
buffer:
time = 1153000
data = 1550456016
buffer:
time = 1249000
data = 1657906039
buffer:
time = 1345000
data = -762677083
buffer:
time = 1441000
data = -1343810763
buffer:
time = 1537000
data = 1137318783
buffer:
time = 1633000
data = -1891318229
buffer:
time = 1729000
data = -472068495
buffer:
time = 1825000
data = 832315001
buffer:
time = 1921000
data = 2054935175
buffer:
time = 2017000
data = 57921641
buffer:
time = 2113000
data = 2132759067
buffer:
time = 2209000
data = -1742540521
buffer:
time = 2305000
data = 1657024301
buffer:
time = 2401000
data = -585080145
buffer:
time = 2497000
data = 427271397
buffer:
time = 2593000
data = -364201340
buffer:
time = 2689000
data = -627965287
config:
encoding = 536870912 (24 bit)
channel count = 2
sample rate = 48000
buffer:
time = 0
data = 225023649
buffer:
time = 96000
data = 455106306
buffer:
time = 192000
data = 2025727297
buffer:
time = 288000
data = 758514657
buffer:
time = 384000
data = 1044986473
buffer:
time = 480000
data = -2030029695
buffer:
time = 576000
data = 1907053281
buffer:
time = 672000
data = -1974954431
buffer:
time = 768000
data = -206248383
buffer:
time = 864000
data = 1484984417
buffer:
time = 960000
data = -1306117439
buffer:
time = 1056000
data = 692829792
buffer:
time = 1152000
data = 1070563058
buffer:
time = 1248000
data = -1444096479
buffer:
time = 1344000
data = 1753016419
buffer:
time = 1440000
data = 1947797953
buffer:
time = 1536000
data = 266121411
buffer:
time = 1632000
data = 1275494369
buffer:
time = 1728000
data = 372077825
buffer:
time = 1824000
data = -993079679
buffer:
time = 1920000
data = 177307937
buffer:
time = 2016000
data = 2037083009
buffer:
time = 2112000
data = -435776287
buffer:
time = 2208000
data = 1867447329
buffer:
time = 2304000
data = 1884495937
buffer:
time = 2400000
data = -804673375
buffer:
time = 2496000
data = -588531007
buffer:
time = 2592000
data = -1064642970
buffer:
time = 2688000
data = -1771406207
...@@ -24,6 +24,7 @@ track 0: ...@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0 selectionFlags = 0
language = null language = null
drmInitData = - drmInitData = -
metadata = null
initializationData: initializationData:
total output bytes = 526272 total output bytes = 526272
sample count = 33 sample count = 33
......
...@@ -24,6 +24,7 @@ track 0: ...@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0 selectionFlags = 0
language = null language = null
drmInitData = - drmInitData = -
metadata = null
initializationData: initializationData:
total output bytes = 362432 total output bytes = 362432
sample count = 23 sample count = 23
......
...@@ -24,6 +24,7 @@ track 0: ...@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0 selectionFlags = 0
language = null language = null
drmInitData = - drmInitData = -
metadata = null
initializationData: initializationData:
total output bytes = 182208 total output bytes = 182208
sample count = 12 sample count = 12
......
...@@ -24,6 +24,7 @@ track 0: ...@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0 selectionFlags = 0
language = null language = null
drmInitData = - drmInitData = -
metadata = null
initializationData: initializationData:
total output bytes = 18368 total output bytes = 18368
sample count = 2 sample count = 2
......
...@@ -24,6 +24,7 @@ track 0: ...@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0 selectionFlags = 0
language = null language = null
drmInitData = - drmInitData = -
metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=]
initializationData: initializationData:
total output bytes = 526272 total output bytes = 526272
sample count = 33 sample count = 33
......
...@@ -24,6 +24,7 @@ track 0: ...@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0 selectionFlags = 0
language = null language = null
drmInitData = - drmInitData = -
metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=]
initializationData: initializationData:
total output bytes = 362432 total output bytes = 362432
sample count = 23 sample count = 23
......
...@@ -24,6 +24,7 @@ track 0: ...@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0 selectionFlags = 0
language = null language = null
drmInitData = - drmInitData = -
metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=]
initializationData: initializationData:
total output bytes = 182208 total output bytes = 182208
sample count = 12 sample count = 12
......
...@@ -24,6 +24,7 @@ track 0: ...@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0 selectionFlags = 0
language = null language = null
drmInitData = - drmInitData = -
metadata = entries=[TXXX: description=ID: value=105519843, TIT2: description=null: value=那么爱你为什么, TPE1: description=null: value=阿强, TALB: description=null: value=华丽的外衣, TXXX: description=ID: value=105519843, APIC: mimeType=image/jpeg, description=]
initializationData: initializationData:
total output bytes = 18368 total output bytes = 18368
sample count = 2 sample count = 2
......
...@@ -20,10 +20,12 @@ import static org.junit.Assert.fail; ...@@ -20,10 +20,12 @@ import static org.junit.Assert.fail;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ext.flac.FlacBinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -47,19 +49,20 @@ public final class FlacBinarySearchSeekerTest { ...@@ -47,19 +49,20 @@ public final class FlacBinarySearchSeekerTest {
throws IOException, FlacDecoderException, InterruptedException { throws IOException, FlacDecoderException, InterruptedException {
byte[] data = byte[] data =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC); TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni(); FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input); decoderJni.setData(input);
OutputFrameHolder outputFrameHolder = new OutputFrameHolder(ByteBuffer.allocateDirect(0));
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeStreamMetadata(), decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0, /* firstFramePosition= */ 0,
data.length, data.length,
decoderJni); decoderJni,
outputFrameHolder);
SeekMap seekMap = seeker.getSeekMap(); SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull(); assertThat(seekMap).isNotNull();
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US); assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
assertThat(seekMap.isSeekable()).isTrue(); assertThat(seekMap.isSeekable()).isTrue();
...@@ -70,18 +73,20 @@ public final class FlacBinarySearchSeekerTest { ...@@ -70,18 +73,20 @@ public final class FlacBinarySearchSeekerTest {
throws IOException, FlacDecoderException, InterruptedException { throws IOException, FlacDecoderException, InterruptedException {
byte[] data = byte[] data =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC); TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni(); FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input); decoderJni.setData(input);
OutputFrameHolder outputFrameHolder = new OutputFrameHolder(ByteBuffer.allocateDirect(0));
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeStreamMetadata(), decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0, /* firstFramePosition= */ 0,
data.length, data.length,
decoderJni); decoderJni,
outputFrameHolder);
seeker.setSeekTargetUs(/* timeUs= */ 1000); seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue(); assertThat(seeker.isSeeking()).isTrue();
} }
} }
...@@ -25,9 +25,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; ...@@ -25,9 +25,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.testutil.CapturingAudioSink;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -37,7 +41,8 @@ import org.junit.runner.RunWith; ...@@ -37,7 +41,8 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class FlacPlaybackTest { public class FlacPlaybackTest {
private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka"; private static final String BEAR_FLAC_16BIT = "bear-flac-16bit.mka";
private static final String BEAR_FLAC_24BIT = "bear-flac-24bit.mka";
@Before @Before
public void setUp() { public void setUp() {
...@@ -47,38 +52,56 @@ public class FlacPlaybackTest { ...@@ -47,38 +52,56 @@ public class FlacPlaybackTest {
} }
@Test @Test
public void testBasicPlayback() throws Exception { public void test16BitPlayback() throws Exception {
playUri(BEAR_FLAC_URI); playAndAssertAudioSinkInput(BEAR_FLAC_16BIT);
} }
private void playUri(String uri) throws Exception { @Test
public void test24BitPlayback() throws Exception {
playAndAssertAudioSinkInput(BEAR_FLAC_24BIT);
}
private static void playAndAssertAudioSinkInput(String fileName) throws Exception {
CapturingAudioSink audioSink =
new CapturingAudioSink(
new DefaultAudioSink(/* audioCapabilities= */ null, new AudioProcessor[0]));
TestPlaybackRunnable testPlaybackRunnable = TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext()); new TestPlaybackRunnable(
Uri.parse("asset:///" + fileName),
ApplicationProvider.getApplicationContext(),
audioSink);
Thread thread = new Thread(testPlaybackRunnable); Thread thread = new Thread(testPlaybackRunnable);
thread.start(); thread.start();
thread.join(); thread.join();
if (testPlaybackRunnable.playbackException != null) { if (testPlaybackRunnable.playbackException != null) {
throw testPlaybackRunnable.playbackException; throw testPlaybackRunnable.playbackException;
} }
audioSink.assertOutput(
ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump");
} }
private static class TestPlaybackRunnable implements Player.EventListener, Runnable { private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
private final Context context; private final Context context;
private final Uri uri; private final Uri uri;
private final AudioSink audioSink;
private ExoPlayer player; private ExoPlayer player;
private ExoPlaybackException playbackException; private ExoPlaybackException playbackException;
public TestPlaybackRunnable(Uri uri, Context context) { public TestPlaybackRunnable(Uri uri, Context context, AudioSink audioSink) {
this.uri = uri; this.uri = uri;
this.context = context; this.context = context;
this.audioSink = audioSink;
} }
@Override @Override
public void run() { public void run() {
Looper.prepare(); Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer(); LibflacAudioRenderer audioRenderer =
new LibflacAudioRenderer(/* eventHandler= */ null, /* eventListener= */ null, audioSink);
player = new ExoPlayer.Builder(context, audioRenderer).build(); player = new ExoPlayer.Builder(context, audioRenderer).build();
player.addListener(this); player.addListener(this);
MediaSource mediaSource = MediaSource mediaSource =
...@@ -105,5 +128,4 @@ public class FlacPlaybackTest { ...@@ -105,5 +128,4 @@ public class FlacPlaybackTest {
} }
} }
} }
} }
...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker; ...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacConstants;
import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -31,23 +32,50 @@ import java.nio.ByteBuffer; ...@@ -31,23 +32,50 @@ import java.nio.ByteBuffer;
*/ */
/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker { /* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker {
/**
* Holds a frame extracted from a stream, together with the time stamp of the frame in
* microseconds.
*/
public static final class OutputFrameHolder {
public final ByteBuffer byteBuffer;
public long timeUs;
/** Constructs an instance, wrapping the given byte buffer. */
public OutputFrameHolder(ByteBuffer outputByteBuffer) {
this.timeUs = 0;
this.byteBuffer = outputByteBuffer;
}
}
private final FlacDecoderJni decoderJni; private final FlacDecoderJni decoderJni;
/**
* Creates a {@link FlacBinarySearchSeeker}.
*
* @param streamMetadata The stream metadata.
* @param firstFramePosition The byte offset of the first frame in the stream.
* @param inputLength The length of the stream in bytes.
* @param decoderJni The FLAC JNI decoder.
* @param outputFrameHolder A holder used to retrieve the frame found by a seeking operation.
*/
public FlacBinarySearchSeeker( public FlacBinarySearchSeeker(
FlacStreamMetadata streamMetadata, FlacStreamMetadata streamMetadata,
long firstFramePosition, long firstFramePosition,
long inputLength, long inputLength,
FlacDecoderJni decoderJni) { FlacDecoderJni decoderJni,
OutputFrameHolder outputFrameHolder) {
super( super(
new FlacSeekTimestampConverter(streamMetadata), /* seekTimestampConverter= */ streamMetadata::getSampleNumber,
new FlacTimestampSeeker(decoderJni), new FlacTimestampSeeker(decoderJni, outputFrameHolder),
streamMetadata.durationUs(), streamMetadata.getDurationUs(),
/* floorTimePosition= */ 0, /* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamMetadata.totalSamples, /* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition, /* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength, /* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(), /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize)); /* minimumSearchRange= */ Math.max(
FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni); this.decoderJni = Assertions.checkNotNull(decoderJni);
} }
...@@ -63,14 +91,15 @@ import java.nio.ByteBuffer; ...@@ -63,14 +91,15 @@ import java.nio.ByteBuffer;
private static final class FlacTimestampSeeker implements TimestampSeeker { private static final class FlacTimestampSeeker implements TimestampSeeker {
private final FlacDecoderJni decoderJni; private final FlacDecoderJni decoderJni;
private final OutputFrameHolder outputFrameHolder;
private FlacTimestampSeeker(FlacDecoderJni decoderJni) { private FlacTimestampSeeker(FlacDecoderJni decoderJni, OutputFrameHolder outputFrameHolder) {
this.decoderJni = decoderJni; this.decoderJni = decoderJni;
this.outputFrameHolder = outputFrameHolder;
} }
@Override @Override
public TimestampSearchResult searchForTimestamp( public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleIndex)
ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException { throws IOException, InterruptedException {
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer; ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
long searchPosition = input.getPosition(); long searchPosition = input.getPosition();
...@@ -97,6 +126,8 @@ import java.nio.ByteBuffer; ...@@ -97,6 +126,8 @@ import java.nio.ByteBuffer;
if (targetSampleInLastFrame) { if (targetSampleInLastFrame) {
// We are holding the target frame in outputFrameHolder. Set its presentation time now. // We are holding the target frame in outputFrameHolder. Set its presentation time now.
outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp(); outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp();
// The input position is passed even though it does not indicate the frame containing the
// target sample because the extractor must continue to read from this position.
return TimestampSearchResult.targetFoundResult(input.getPosition()); return TimestampSearchResult.targetFoundResult(input.getPosition());
} else if (nextFrameSampleIndex <= targetSampleIndex) { } else if (nextFrameSampleIndex <= targetSampleIndex) {
return TimestampSearchResult.underestimatedResult( return TimestampSearchResult.underestimatedResult(
...@@ -106,21 +137,4 @@ import java.nio.ByteBuffer; ...@@ -106,21 +137,4 @@ import java.nio.ByteBuffer;
} }
} }
} }
/**
* A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as
* the timestamp for a stream seek time position.
*/
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
private final FlacStreamMetadata streamMetadata;
public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
this.streamMetadata = streamMetadata;
}
@Override
public long timeUsToTargetTime(long timeUs) {
return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
}
}
} }
...@@ -33,7 +33,7 @@ import java.util.List; ...@@ -33,7 +33,7 @@ import java.util.List;
/* package */ final class FlacDecoder extends /* package */ final class FlacDecoder extends
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FlacDecoderException> { SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FlacDecoderException> {
private final int maxOutputBufferSize; private final FlacStreamMetadata streamMetadata;
private final FlacDecoderJni decoderJni; private final FlacDecoderJni decoderJni;
/** /**
...@@ -59,7 +59,6 @@ import java.util.List; ...@@ -59,7 +59,6 @@ import java.util.List;
} }
decoderJni = new FlacDecoderJni(); decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
FlacStreamMetadata streamMetadata;
try { try {
streamMetadata = decoderJni.decodeStreamMetadata(); streamMetadata = decoderJni.decodeStreamMetadata();
} catch (ParserException e) { } catch (ParserException e) {
...@@ -72,7 +71,6 @@ import java.util.List; ...@@ -72,7 +71,6 @@ import java.util.List;
int initialInputBufferSize = int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize; maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize); setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
} }
@Override @Override
...@@ -103,7 +101,8 @@ import java.util.List; ...@@ -103,7 +101,8 @@ import java.util.List;
decoderJni.flush(); decoderJni.flush();
} }
decoderJni.setData(Util.castNonNull(inputBuffer.data)); decoderJni.setData(Util.castNonNull(inputBuffer.data));
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); ByteBuffer outputData =
outputBuffer.init(inputBuffer.timeUs, streamMetadata.getMaxDecodedFrameSize());
try { try {
decoderJni.decodeSample(outputData); decoderJni.decodeSample(outputData);
} catch (FlacDecoderJni.FlacFrameDecodeException e) { } catch (FlacDecoderJni.FlacFrameDecodeException e) {
...@@ -121,4 +120,8 @@ import java.util.List; ...@@ -121,4 +120,8 @@ import java.util.List;
decoderJni.release(); decoderJni.release();
} }
/** Returns the {@link FlacStreamMetadata} decoded from the initialization data. */
public FlacStreamMetadata getStreamMetadata() {
return streamMetadata;
}
} }
...@@ -21,18 +21,17 @@ import androidx.annotation.IntDef; ...@@ -21,18 +21,17 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder; import com.google.android.exoplayer2.ext.flac.FlacBinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.Id3Peeker; import com.google.android.exoplayer2.extractor.FlacMetadataReader;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
...@@ -42,7 +41,6 @@ import java.lang.annotation.Documented; ...@@ -42,7 +41,6 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...@@ -72,11 +70,7 @@ public final class FlacExtractor implements Extractor { ...@@ -72,11 +70,7 @@ public final class FlacExtractor implements Extractor {
*/ */
public static final int FLAG_DISABLE_ID3_METADATA = 1; public static final int FLAG_DISABLE_ID3_METADATA = 1;
/** FLAC stream marker */
private static final byte[] FLAC_STREAM_MARKER = {'f', 'L', 'a', 'C'};
private final ParsableByteArray outputBuffer; private final ParsableByteArray outputBuffer;
private final Id3Peeker id3Peeker;
private final boolean id3MetadataDisabled; private final boolean id3MetadataDisabled;
@Nullable private FlacDecoderJni decoderJni; @Nullable private FlacDecoderJni decoderJni;
...@@ -90,7 +84,7 @@ public final class FlacExtractor implements Extractor { ...@@ -90,7 +84,7 @@ public final class FlacExtractor implements Extractor {
@Nullable private Metadata id3Metadata; @Nullable private Metadata id3Metadata;
@Nullable private FlacBinarySearchSeeker binarySearchSeeker; @Nullable private FlacBinarySearchSeeker binarySearchSeeker;
/** Constructs an instance with flags = 0. */ /** Constructs an instance with {@code flags = 0}. */
public FlacExtractor() { public FlacExtractor() {
this(/* flags= */ 0); this(/* flags= */ 0);
} }
...@@ -98,11 +92,11 @@ public final class FlacExtractor implements Extractor { ...@@ -98,11 +92,11 @@ public final class FlacExtractor implements Extractor {
/** /**
* Constructs an instance. * Constructs an instance.
* *
* @param flags Flags that control the extractor's behavior. * @param flags Flags that control the extractor's behavior. Possible flags are described by
* {@link Flags}.
*/ */
public FlacExtractor(int flags) { public FlacExtractor(int flags) {
outputBuffer = new ParsableByteArray(); outputBuffer = new ParsableByteArray();
id3Peeker = new Id3Peeker();
id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
} }
...@@ -120,17 +114,15 @@ public final class FlacExtractor implements Extractor { ...@@ -120,17 +114,15 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
if (input.getPosition() == 0) { id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
id3Metadata = peekId3Data(input); return FlacMetadataReader.checkAndPeekStreamMarker(input);
}
return peekFlacStreamMarker(input);
} }
@Override @Override
public int read(final ExtractorInput input, PositionHolder seekPosition) public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) { if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input); id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true);
} }
FlacDecoderJni decoderJni = initDecoderJni(input); FlacDecoderJni decoderJni = initDecoderJni(input);
...@@ -182,19 +174,6 @@ public final class FlacExtractor implements Extractor { ...@@ -182,19 +174,6 @@ public final class FlacExtractor implements Extractor {
} }
} }
/**
* Peeks ID3 tag data at the beginning of the input.
*
* @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.
*/
@Nullable
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
input.resetPeekPosition();
Id3Decoder.FramePredicate id3FramePredicate =
id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
return id3Peeker.peekId3Data(input, id3FramePredicate);
}
@EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized. @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
@SuppressWarnings({"contracts.postcondition.not.satisfied"}) @SuppressWarnings({"contracts.postcondition.not.satisfied"})
private FlacDecoderJni initDecoderJni(ExtractorInput input) { private FlacDecoderJni initDecoderJni(ExtractorInput input) {
...@@ -211,11 +190,12 @@ public final class FlacExtractor implements Extractor { ...@@ -211,11 +190,12 @@ public final class FlacExtractor implements Extractor {
return; return;
} }
FlacDecoderJni flacDecoderJni = decoderJni;
FlacStreamMetadata streamMetadata; FlacStreamMetadata streamMetadata;
try { try {
streamMetadata = decoderJni.decodeStreamMetadata(); streamMetadata = flacDecoderJni.decodeStreamMetadata();
} catch (IOException e) { } catch (IOException e) {
decoderJni.reset(/* newPosition= */ 0); flacDecoderJni.reset(/* newPosition= */ 0);
input.setRetryPosition(/* position= */ 0, e); input.setRetryPosition(/* position= */ 0, e);
throw e; throw e;
} }
...@@ -223,15 +203,18 @@ public final class FlacExtractor implements Extractor { ...@@ -223,15 +203,18 @@ public final class FlacExtractor implements Extractor {
streamMetadataDecoded = true; streamMetadataDecoded = true;
if (this.streamMetadata == null) { if (this.streamMetadata == null) {
this.streamMetadata = streamMetadata; this.streamMetadata = streamMetadata;
outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
binarySearchSeeker = binarySearchSeeker =
outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); outputSeekMap(
Metadata metadata = id3MetadataDisabled ? null : id3Metadata; flacDecoderJni,
if (streamMetadata.metadata != null) { streamMetadata,
metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata); input.getLength(),
} extractorOutput,
outputFrameHolder);
@Nullable
Metadata metadata = streamMetadata.getMetadataCopyWithAppendedEntriesFrom(id3Metadata);
outputFormat(streamMetadata, metadata, trackOutput); outputFormat(streamMetadata, metadata, trackOutput);
outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
} }
} }
...@@ -243,7 +226,7 @@ public final class FlacExtractor implements Extractor { ...@@ -243,7 +226,7 @@ public final class FlacExtractor implements Extractor {
OutputFrameHolder outputFrameHolder, OutputFrameHolder outputFrameHolder,
TrackOutput trackOutput) TrackOutput trackOutput)
throws InterruptedException, IOException { throws InterruptedException, IOException {
int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition);
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput); outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
...@@ -252,18 +235,6 @@ public final class FlacExtractor implements Extractor { ...@@ -252,18 +235,6 @@ public final class FlacExtractor implements Extractor {
} }
/** /**
* Peeks from the beginning of the input to see if {@link #FLAC_STREAM_MARKER} is present.
*
* @return Whether the input begins with {@link #FLAC_STREAM_MARKER}.
*/
private static boolean peekFlacStreamMarker(ExtractorInput input)
throws IOException, InterruptedException {
byte[] header = new byte[FLAC_STREAM_MARKER.length];
input.peekFully(header, /* offset= */ 0, FLAC_STREAM_MARKER.length);
return Arrays.equals(header, FLAC_STREAM_MARKER);
}
/**
* Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
* handle seeks. * handle seeks.
*/ */
...@@ -272,19 +243,21 @@ public final class FlacExtractor implements Extractor { ...@@ -272,19 +243,21 @@ public final class FlacExtractor implements Extractor {
FlacDecoderJni decoderJni, FlacDecoderJni decoderJni,
FlacStreamMetadata streamMetadata, FlacStreamMetadata streamMetadata,
long streamLength, long streamLength,
ExtractorOutput output) { ExtractorOutput output,
OutputFrameHolder outputFrameHolder) {
boolean haveSeekTable = decoderJni.getSeekPoints(/* timeUs= */ 0) != null; boolean haveSeekTable = decoderJni.getSeekPoints(/* timeUs= */ 0) != null;
FlacBinarySearchSeeker binarySearchSeeker = null; FlacBinarySearchSeeker binarySearchSeeker = null;
SeekMap seekMap; SeekMap seekMap;
if (haveSeekTable) { if (haveSeekTable) {
seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni); seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni);
} else if (streamLength != C.LENGTH_UNSET) { } else if (streamLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition(); long firstFramePosition = decoderJni.getDecodePosition();
binarySearchSeeker = binarySearchSeeker =
new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni); new FlacBinarySearchSeeker(
streamMetadata, firstFramePosition, streamLength, decoderJni, outputFrameHolder);
seekMap = binarySearchSeeker.getSeekMap(); seekMap = binarySearchSeeker.getSeekMap();
} else { } else {
seekMap = new SeekMap.Unseekable(streamMetadata.durationUs()); seekMap = new SeekMap.Unseekable(streamMetadata.getDurationUs());
} }
output.seekMap(seekMap); output.seekMap(seekMap);
return binarySearchSeeker; return binarySearchSeeker;
...@@ -297,8 +270,8 @@ public final class FlacExtractor implements Extractor { ...@@ -297,8 +270,8 @@ public final class FlacExtractor implements Extractor {
/* id= */ null, /* id= */ null,
MimeTypes.AUDIO_RAW, MimeTypes.AUDIO_RAW,
/* codecs= */ null, /* codecs= */ null,
streamMetadata.bitRate(), streamMetadata.getBitRate(),
streamMetadata.maxDecodedFrameSize(), streamMetadata.getMaxDecodedFrameSize(),
streamMetadata.channels, streamMetadata.channels,
streamMetadata.sampleRate, streamMetadata.sampleRate,
getPcmEncoding(streamMetadata.bitsPerSample), getPcmEncoding(streamMetadata.bitsPerSample),
......
...@@ -21,18 +21,24 @@ import com.google.android.exoplayer2.C; ...@@ -21,18 +21,24 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacConstants;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /** Decodes and renders audio using the native Flac decoder. */
* Decodes and renders audio using the native Flac decoder. public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
*/
public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
private static final int NUM_BUFFERS = 16; private static final int NUM_BUFFERS = 16;
@MonotonicNonNull private FlacStreamMetadata streamMetadata;
public LibflacAudioRenderer() { public LibflacAudioRenderer() {
this(/* eventHandler= */ null, /* eventListener= */ null); this(/* eventHandler= */ null, /* eventListener= */ null);
} }
...@@ -50,6 +56,24 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -50,6 +56,24 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
super(eventHandler, eventListener, audioProcessors); super(eventHandler, eventListener, audioProcessors);
} }
/**
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioSink The sink to which audio will be output.
*/
public LibflacAudioRenderer(
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioSink audioSink) {
super(
eventHandler,
eventListener,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false,
audioSink);
}
@Override @Override
@FormatSupport @FormatSupport
protected int supportsFormatInternal( protected int supportsFormatInternal(
...@@ -57,7 +81,23 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -57,7 +81,23 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
if (!FlacLibrary.isAvailable() if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { }
// Compute the PCM encoding that the FLAC decoder will output.
@C.PcmEncoding int pcmEncoding;
if (format.initializationData.isEmpty()) {
// The initialization data might not be set if the format was obtained from a manifest (e.g.
// for DASH playbacks) rather than directly from the media. In this case we assume
// ENCODING_PCM_16BIT. If the actual encoding is different then playback will still succeed as
// long as the AudioSink supports it, which will always be true when using DefaultAudioSink.
pcmEncoding = C.ENCODING_PCM_16BIT;
} else {
int streamMetadataOffset =
FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE;
FlacStreamMetadata streamMetadata =
new FlacStreamMetadata(format.initializationData.get(0), streamMetadataOffset);
pcmEncoding = Util.getPcmEncoding(streamMetadata.bitsPerSample);
}
if (!supportsOutput(format.channelCount, pcmEncoding)) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
...@@ -69,8 +109,27 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -69,8 +109,27 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
@Override @Override
protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FlacDecoderException { throws FlacDecoderException {
return new FlacDecoder( FlacDecoder decoder =
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
streamMetadata = decoder.getStreamMetadata();
return decoder;
} }
@Override
protected Format getOutputFormat() {
Assertions.checkNotNull(streamMetadata);
return Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
/* maxInputSize= */ Format.NO_VALUE,
streamMetadata.channels,
streamMetadata.sampleRate,
Util.getPcmEncoding(streamMetadata.bitsPerSample),
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
}
} }
...@@ -151,7 +151,7 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { ...@@ -151,7 +151,7 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
"FlacStreamMetadata"); "FlacStreamMetadata");
jmethodID flacStreamMetadataConstructor = jmethodID flacStreamMetadataConstructor =
env->GetMethodID(flacStreamMetadataClass, "<init>", env->GetMethodID(flacStreamMetadataClass, "<init>",
"(IIIIIIIJLjava/util/List;Ljava/util/List;)V"); "(IIIIIIIJLjava/util/ArrayList;Ljava/util/ArrayList;)V");
return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor, return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
streamInfo.min_blocksize, streamInfo.max_blocksize, streamInfo.min_blocksize, streamInfo.max_blocksize,
......
...@@ -349,26 +349,6 @@ bool FLACParser::decodeMetadata() { ...@@ -349,26 +349,6 @@ bool FLACParser::decodeMetadata() {
ALOGE("unsupported bits per sample %u", getBitsPerSample()); ALOGE("unsupported bits per sample %u", getBitsPerSample());
return false; return false;
} }
// check sample rate
switch (getSampleRate()) {
case 8000:
case 11025:
case 12000:
case 16000:
case 22050:
case 24000:
case 32000:
case 44100:
case 48000:
case 88200:
case 96000:
case 176400:
case 192000:
break;
default:
ALOGE("unsupported sample rate %u", getSampleRate());
return false;
}
// configure the appropriate copy function based on device endianness. // configure the appropriate copy function based on device endianness.
if (isBigEndian()) { if (isBigEndian()) {
mCopy = copyToByteArrayBigEndian; mCopy = copyToByteArrayBigEndian;
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.flac;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
import com.google.android.exoplayer2.extractor.ogg.OggExtractor;
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
import com.google.android.exoplayer2.extractor.ts.Ac4Extractor;
import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
import com.google.android.exoplayer2.extractor.ts.PsExtractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.extractor.wav.WavExtractor;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link DefaultExtractorsFactory}. */
@RunWith(AndroidJUnit4.class)
public final class DefaultExtractorsFactoryTest {
@Test
public void testCreateExtractors_returnExpectedClasses() {
DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory();
Extractor[] extractors = defaultExtractorsFactory.createExtractors();
List<Class<?>> listCreatedExtractorClasses = new ArrayList<>();
for (Extractor extractor : extractors) {
listCreatedExtractorClasses.add(extractor.getClass());
}
Class<?>[] expectedExtractorClassses =
new Class<?>[] {
MatroskaExtractor.class,
FragmentedMp4Extractor.class,
Mp4Extractor.class,
Mp3Extractor.class,
AdtsExtractor.class,
Ac3Extractor.class,
Ac4Extractor.class,
TsExtractor.class,
FlvExtractor.class,
OggExtractor.class,
PsExtractor.class,
WavExtractor.class,
AmrExtractor.class,
FlacExtractor.class
};
assertThat(listCreatedExtractorClasses).containsNoDuplicates();
assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses);
}
}
...@@ -76,8 +76,8 @@ public final class JobDispatcherScheduler implements Scheduler { ...@@ -76,8 +76,8 @@ public final class JobDispatcherScheduler implements Scheduler {
* {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called. * {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.
*/ */
public JobDispatcherScheduler(Context context, String jobTag) { public JobDispatcherScheduler(Context context, String jobTag) {
this.jobDispatcher = context = context.getApplicationContext();
new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext())); this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context));
this.jobTag = jobTag; this.jobTag = jobTag;
} }
......
...@@ -178,8 +178,8 @@ public final class MediaSessionConnector { ...@@ -178,8 +178,8 @@ public final class MediaSessionConnector {
Player player, Player player,
ControlDispatcher controlDispatcher, ControlDispatcher controlDispatcher,
String command, String command,
Bundle extras, @Nullable Bundle extras,
ResultReceiver cb); @Nullable ResultReceiver cb);
} }
/** Interface to which playback preparation and play actions are delegated. */ /** Interface to which playback preparation and play actions are delegated. */
...@@ -394,6 +394,7 @@ public final class MediaSessionConnector { ...@@ -394,6 +394,7 @@ public final class MediaSessionConnector {
* @param player The player connected to the media session. * @param player The player connected to the media session.
* @return The custom action to be included in the session playback state or {@code null}. * @return The custom action to be included in the session playback state or {@code null}.
*/ */
@Nullable
PlaybackStateCompat.CustomAction getCustomAction(Player player); PlaybackStateCompat.CustomAction getCustomAction(Player player);
} }
...@@ -1293,7 +1294,7 @@ public final class MediaSessionConnector { ...@@ -1293,7 +1294,7 @@ public final class MediaSessionConnector {
} }
@Override @Override
public void onCommand(String command, Bundle extras, ResultReceiver cb) { public void onCommand(String command, @Nullable Bundle extras, @Nullable ResultReceiver cb) {
if (player != null) { if (player != null) {
for (int i = 0; i < commandReceivers.size(); i++) { for (int i = 0; i < commandReceivers.size(); i++) {
if (commandReceivers.get(i).onCommand(player, controlDispatcher, command, extras, cb)) { if (commandReceivers.get(i).onCommand(player, controlDispatcher, command, extras, cb)) {
......
...@@ -191,9 +191,9 @@ public final class TimelineQueueEditor ...@@ -191,9 +191,9 @@ public final class TimelineQueueEditor
Player player, Player player,
ControlDispatcher controlDispatcher, ControlDispatcher controlDispatcher,
String command, String command,
Bundle extras, @Nullable Bundle extras,
ResultReceiver cb) { @Nullable ResultReceiver cb) {
if (!COMMAND_MOVE_QUEUE_ITEM.equals(command)) { if (!COMMAND_MOVE_QUEUE_ITEM.equals(command) || extras == null) {
return false; return false;
} }
int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET); int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET);
......
...@@ -186,8 +186,8 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu ...@@ -186,8 +186,8 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
Player player, Player player,
ControlDispatcher controlDispatcher, ControlDispatcher controlDispatcher,
String command, String command,
Bundle extras, @Nullable Bundle extras,
ResultReceiver cb) { @Nullable ResultReceiver cb) {
return false; return false;
} }
......
...@@ -39,9 +39,9 @@ dependencies { ...@@ -39,9 +39,9 @@ dependencies {
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
// Do not update to 3.13.X or later until minSdkVersion is increased to 21: // Do not update to 3.13.X or later until minSdkVersion is increased to 21:
// https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5 // https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5
// Since OkHttp is distributed as a jar rather than an aar, Gradle wont stop // Since OkHttp is distributed as a jar rather than an aar, Gradle won't
// us from making this mistake! // stop us from making this mistake!
api 'com.squareup.okhttp3:okhttp:3.12.5' api 'com.squareup.okhttp3:okhttp:3.12.8'
} }
ext { ext {
......
...@@ -5,6 +5,12 @@ ...@@ -5,6 +5,12 @@
public static android.net.Uri buildRawResourceUri(int); public static android.net.Uri buildRawResourceUri(int);
} }
# Methods accessed via reflection in DefaultExtractorsFactory
-dontnote com.google.android.exoplayer2.ext.flac.FlacLibrary
-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacLibrary {
public static boolean isAvailable();
}
# Some members of this class are being accessed from native methods. Keep them unobfuscated. # Some members of this class are being accessed from native methods. Keep them unobfuscated.
-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer { -keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer {
*; *;
......
...@@ -95,14 +95,16 @@ public final class C { ...@@ -95,14 +95,16 @@ public final class C {
* The name of the ASCII charset. * The name of the ASCII charset.
*/ */
public static final String ASCII_NAME = "US-ASCII"; public static final String ASCII_NAME = "US-ASCII";
/** /**
* The name of the UTF-8 charset. * The name of the UTF-8 charset.
*/ */
public static final String UTF8_NAME = "UTF-8"; public static final String UTF8_NAME = "UTF-8";
/** /** The name of the ISO-8859-1 charset. */
* The name of the UTF-16 charset. public static final String ISO88591_NAME = "ISO-8859-1";
*/
/** The name of the UTF-16 charset. */
public static final String UTF16_NAME = "UTF-16"; public static final String UTF16_NAME = "UTF-16";
/** The name of the UTF-16 little-endian charset. */ /** The name of the UTF-16 little-endian charset. */
...@@ -148,8 +150,8 @@ public final class C { ...@@ -148,8 +150,8 @@ public final class C {
/** /**
* Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE},
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link
* #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},
* {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
*/ */
...@@ -160,26 +162,26 @@ public final class C { ...@@ -160,26 +162,26 @@ public final class C {
ENCODING_INVALID, ENCODING_INVALID,
ENCODING_PCM_8BIT, ENCODING_PCM_8BIT,
ENCODING_PCM_16BIT, ENCODING_PCM_16BIT,
ENCODING_PCM_16BIT_BIG_ENDIAN,
ENCODING_PCM_24BIT, ENCODING_PCM_24BIT,
ENCODING_PCM_32BIT, ENCODING_PCM_32BIT,
ENCODING_PCM_FLOAT, ENCODING_PCM_FLOAT,
ENCODING_PCM_MU_LAW, ENCODING_MP3,
ENCODING_PCM_A_LAW,
ENCODING_AC3, ENCODING_AC3,
ENCODING_E_AC3, ENCODING_E_AC3,
ENCODING_E_AC3_JOC, ENCODING_E_AC3_JOC,
ENCODING_AC4, ENCODING_AC4,
ENCODING_DTS, ENCODING_DTS,
ENCODING_DTS_HD, ENCODING_DTS_HD,
ENCODING_DOLBY_TRUEHD, ENCODING_DOLBY_TRUEHD
}) })
public @interface Encoding {} public @interface Encoding {}
/** /**
* Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE},
* {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
* #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. * {@link #ENCODING_PCM_FLOAT}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -188,11 +190,10 @@ public final class C { ...@@ -188,11 +190,10 @@ public final class C {
ENCODING_INVALID, ENCODING_INVALID,
ENCODING_PCM_8BIT, ENCODING_PCM_8BIT,
ENCODING_PCM_16BIT, ENCODING_PCM_16BIT,
ENCODING_PCM_16BIT_BIG_ENDIAN,
ENCODING_PCM_24BIT, ENCODING_PCM_24BIT,
ENCODING_PCM_32BIT, ENCODING_PCM_32BIT,
ENCODING_PCM_FLOAT, ENCODING_PCM_FLOAT
ENCODING_PCM_MU_LAW,
ENCODING_PCM_A_LAW
}) })
public @interface PcmEncoding {} public @interface PcmEncoding {}
/** @see AudioFormat#ENCODING_INVALID */ /** @see AudioFormat#ENCODING_INVALID */
...@@ -201,16 +202,16 @@ public final class C { ...@@ -201,16 +202,16 @@ public final class C {
public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
/** @see AudioFormat#ENCODING_PCM_16BIT */ /** @see AudioFormat#ENCODING_PCM_16BIT */
public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
/** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */
public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000;
/** PCM encoding with 24 bits per sample. */ /** PCM encoding with 24 bits per sample. */
public static final int ENCODING_PCM_24BIT = 0x80000000; public static final int ENCODING_PCM_24BIT = 0x20000000;
/** PCM encoding with 32 bits per sample. */ /** PCM encoding with 32 bits per sample. */
public static final int ENCODING_PCM_32BIT = 0x40000000; public static final int ENCODING_PCM_32BIT = 0x30000000;
/** @see AudioFormat#ENCODING_PCM_FLOAT */ /** @see AudioFormat#ENCODING_PCM_FLOAT */
public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
/** Audio encoding for mu-law. */ /** @see AudioFormat#ENCODING_MP3 */
public static final int ENCODING_PCM_MU_LAW = 0x10000000; public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3;
/** Audio encoding for A-law. */
public static final int ENCODING_PCM_A_LAW = 0x20000000;
/** @see AudioFormat#ENCODING_AC3 */ /** @see AudioFormat#ENCODING_AC3 */
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/** @see AudioFormat#ENCODING_E_AC3 */ /** @see AudioFormat#ENCODING_E_AC3 */
...@@ -976,8 +977,8 @@ public final class C { ...@@ -976,8 +977,8 @@ public final class C {
/** /**
* Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE}, * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE},
* {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link
* #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link
* {@link #NETWORK_TYPE_OTHER}. * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -988,6 +989,7 @@ public final class C { ...@@ -988,6 +989,7 @@ public final class C {
NETWORK_TYPE_2G, NETWORK_TYPE_2G,
NETWORK_TYPE_3G, NETWORK_TYPE_3G,
NETWORK_TYPE_4G, NETWORK_TYPE_4G,
NETWORK_TYPE_5G,
NETWORK_TYPE_CELLULAR_UNKNOWN, NETWORK_TYPE_CELLULAR_UNKNOWN,
NETWORK_TYPE_ETHERNET, NETWORK_TYPE_ETHERNET,
NETWORK_TYPE_OTHER NETWORK_TYPE_OTHER
...@@ -1005,6 +1007,8 @@ public final class C { ...@@ -1005,6 +1007,8 @@ public final class C {
public static final int NETWORK_TYPE_3G = 4; public static final int NETWORK_TYPE_3G = 4;
/** Network type for a 4G cellular connection. */ /** Network type for a 4G cellular connection. */
public static final int NETWORK_TYPE_4G = 5; public static final int NETWORK_TYPE_4G = 5;
/** Network type for a 5G cellular connection. */
public static final int NETWORK_TYPE_5G = 9;
/** /**
* Network type for cellular connections which cannot be mapped to one of {@link * Network type for cellular connections which cannot be mapped to one of {@link
* #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}. * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}.
...@@ -1012,10 +1016,7 @@ public final class C { ...@@ -1012,10 +1016,7 @@ public final class C {
public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6; public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6;
/** Network type for an Ethernet connection. */ /** Network type for an Ethernet connection. */
public static final int NETWORK_TYPE_ETHERNET = 7; public static final int NETWORK_TYPE_ETHERNET = 7;
/** /** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */
* Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN,
* Bluetooth).
*/
public static final int NETWORK_TYPE_OTHER = 8; public static final int NETWORK_TYPE_OTHER = 8;
/** /**
......
...@@ -93,8 +93,8 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; ...@@ -93,8 +93,8 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* *
* <p>The figure below shows ExoPlayer's threading model. * <p>The figure below shows ExoPlayer's threading model.
* *
* <p align="center"><img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's threading * <p style="align:center"><img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's
* model"> * threading model">
* *
* <ul> * <ul>
* <li>ExoPlayer instances must be accessed from a single application thread. For the vast * <li>ExoPlayer instances must be accessed from a single application thread. For the vast
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.11.1"; public static final String VERSION = "2.11.2";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.1"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.2";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2011001; public static final int VERSION_INT = 2011002;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -138,13 +138,7 @@ public final class Format implements Parcelable { ...@@ -138,13 +138,7 @@ public final class Format implements Parcelable {
* The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable.
*/ */
public final int sampleRate; public final int sampleRate;
/** /** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */
* The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW}
* then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link
* C#ENCODING_PCM_24BIT}, {@link C#ENCODING_PCM_32BIT}, {@link C#ENCODING_PCM_FLOAT}, {@link
* C#ENCODING_PCM_MU_LAW} or {@link C#ENCODING_PCM_A_LAW}. Set to {@link #NO_VALUE} for other
* media types.
*/
public final @C.PcmEncoding int pcmEncoding; public final @C.PcmEncoding int pcmEncoding;
/** /**
* The number of frames to trim from the start of the decoded audio stream, or 0 if not * The number of frames to trim from the start of the decoded audio stream, or 0 if not
......
...@@ -32,7 +32,8 @@ import java.lang.annotation.RetentionPolicy; ...@@ -32,7 +32,8 @@ import java.lang.annotation.RetentionPolicy;
* valid state transitions are shown below, annotated with the methods that are called during each * valid state transitions are shown below, annotated with the methods that are called during each
* transition. * transition.
* *
* <p align="center"><img src="doc-files/renderer-states.svg" alt="Renderer state transitions"> * <p style="align:center"><img src="doc-files/renderer-states.svg" alt="Renderer state
* transitions">
*/ */
public interface Renderer extends PlayerMessage.Target { public interface Renderer extends PlayerMessage.Target {
......
...@@ -44,7 +44,7 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -44,7 +44,7 @@ import com.google.android.exoplayer2.util.Assertions;
* *
* <h3 id="single-file">Single media file or on-demand stream</h3> * <h3 id="single-file">Single media file or on-demand stream</h3>
* *
* <p align="center"><img src="doc-files/timeline-single-file.svg" alt="Example timeline for a * <p style="align:center"><img src="doc-files/timeline-single-file.svg" alt="Example timeline for a
* single file"> A timeline for a single media file or on-demand stream consists of a single period * single file"> A timeline for a single media file or on-demand stream consists of a single period
* and window. The window spans the whole period, indicating that all parts of the media are * and window. The window spans the whole period, indicating that all parts of the media are
* available for playback. The window's default position is typically at the start of the period * available for playback. The window's default position is typically at the start of the period
...@@ -52,17 +52,17 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -52,17 +52,17 @@ import com.google.android.exoplayer2.util.Assertions;
* *
* <h3>Playlist of media files or on-demand streams</h3> * <h3>Playlist of media files or on-demand streams</h3>
* *
* <p align="center"><img src="doc-files/timeline-playlist.svg" alt="Example timeline for a playlist * <p style="align:center"><img src="doc-files/timeline-playlist.svg" alt="Example timeline for a
* of files"> A timeline for a playlist of media files or on-demand streams consists of multiple * playlist of files"> A timeline for a playlist of media files or on-demand streams consists of
* periods, each with its own window. Each window spans the whole of the corresponding period, and * multiple periods, each with its own window. Each window spans the whole of the corresponding
* typically has a default position at the start of the period. The properties of the periods and * period, and typically has a default position at the start of the period. The properties of the
* windows (e.g. their durations and whether the window is seekable) will often only become known * periods and windows (e.g. their durations and whether the window is seekable) will often only
* when the player starts buffering the corresponding file or stream. * become known when the player starts buffering the corresponding file or stream.
* *
* <h3 id="live-limited">Live stream with limited availability</h3> * <h3 id="live-limited">Live stream with limited availability</h3>
* *
* <p align="center"><img src="doc-files/timeline-live-limited.svg" alt="Example timeline for a live * <p style="align:center"><img src="doc-files/timeline-live-limited.svg" alt="Example timeline for
* stream with limited availability"> A timeline for a live stream consists of a period whose * a live stream with limited availability"> A timeline for a live stream consists of a period whose
* duration is unknown, since it's continually extending as more content is broadcast. If content * duration is unknown, since it's continually extending as more content is broadcast. If content
* only remains available for a limited period of time then the window may start at a non-zero * only remains available for a limited period of time then the window may start at a non-zero
* position, defining the region of content that can still be played. The window will have {@link * position, defining the region of content that can still be played. The window will have {@link
...@@ -72,24 +72,24 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -72,24 +72,24 @@ import com.google.android.exoplayer2.util.Assertions;
* *
* <h3>Live stream with indefinite availability</h3> * <h3>Live stream with indefinite availability</h3>
* *
* <p align="center"><img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline for a * <p style="align:center"><img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline
* live stream with indefinite availability"> A timeline for a live stream with indefinite * for a live stream with indefinite availability"> A timeline for a live stream with indefinite
* availability is similar to the <a href="#live-limited">Live stream with limited availability</a> * availability is similar to the <a href="#live-limited">Live stream with limited availability</a>
* case, except that the window starts at the beginning of the period to indicate that all of the * case, except that the window starts at the beginning of the period to indicate that all of the
* previously broadcast content can still be played. * previously broadcast content can still be played.
* *
* <h3 id="live-multi-period">Live stream with multiple periods</h3> * <h3 id="live-multi-period">Live stream with multiple periods</h3>
* *
* <p align="center"><img src="doc-files/timeline-live-multi-period.svg" alt="Example timeline for a * <p style="align:center"><img src="doc-files/timeline-live-multi-period.svg" alt="Example timeline
* live stream with multiple periods"> This case arises when a live stream is explicitly divided * for a live stream with multiple periods"> This case arises when a live stream is explicitly
* into separate periods, for example at content boundaries. This case is similar to the <a * divided into separate periods, for example at content boundaries. This case is similar to the <a
* href="#live-limited">Live stream with limited availability</a> case, except that the window may * href="#live-limited">Live stream with limited availability</a> case, except that the window may
* span more than one period. Multiple periods are also possible in the indefinite availability * span more than one period. Multiple periods are also possible in the indefinite availability
* case. * case.
* *
* <h3>On-demand stream followed by live stream</h3> * <h3>On-demand stream followed by live stream</h3>
* *
* <p align="center"><img src="doc-files/timeline-advanced.svg" alt="Example timeline for an * <p style="align:center"><img src="doc-files/timeline-advanced.svg" alt="Example timeline for an
* on-demand stream followed by a live stream"> This case is the concatenation of the <a * on-demand stream followed by a live stream"> This case is the concatenation of the <a
* href="#single-file">Single media file or on-demand stream</a> and <a href="#multi-period">Live * href="#single-file">Single media file or on-demand stream</a> and <a href="#multi-period">Live
* stream with multiple periods</a> cases. When playback of the on-demand stream ends, playback of * stream with multiple periods</a> cases. When playback of the on-demand stream ends, playback of
...@@ -97,10 +97,10 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -97,10 +97,10 @@ import com.google.android.exoplayer2.util.Assertions;
* *
* <h3 id="single-file-midrolls">On-demand stream with mid-roll ads</h3> * <h3 id="single-file-midrolls">On-demand stream with mid-roll ads</h3>
* *
* <p align="center"><img src="doc-files/timeline-single-file-midrolls.svg" alt="Example timeline * <p style="align:center"><img src="doc-files/timeline-single-file-midrolls.svg" alt="Example
* for an on-demand stream with mid-roll ad groups"> This case includes mid-roll ad groups, which * timeline for an on-demand stream with mid-roll ad groups"> This case includes mid-roll ad groups,
* are defined as part of the timeline's single period. The period can be queried for information * which are defined as part of the timeline's single period. The period can be queried for
* about the ad groups and the ads they contain. * information about the ad groups and the ads they contain.
*/ */
public abstract class Timeline { public abstract class Timeline {
...@@ -111,7 +111,7 @@ public abstract class Timeline { ...@@ -111,7 +111,7 @@ public abstract class Timeline {
* shows some of the information defined by a window, as well as how this information relates to * shows some of the information defined by a window, as well as how this information relates to
* corresponding {@link Period Periods} in the timeline. * corresponding {@link Period Periods} in the timeline.
* *
* <p align="center"><img src="doc-files/timeline-window.svg" alt="Information defined by a * <p style="align:center"><img src="doc-files/timeline-window.svg" alt="Information defined by a
* timeline window"> * timeline window">
*/ */
public static final class Window { public static final class Window {
...@@ -284,12 +284,12 @@ public abstract class Timeline { ...@@ -284,12 +284,12 @@ public abstract class Timeline {
* Holds information about a period in a {@link Timeline}. A period defines a single logical piece * Holds information about a period in a {@link Timeline}. A period defines a single logical piece
* of media, for example a media file. It may also define groups of ads inserted into the media, * of media, for example a media file. It may also define groups of ads inserted into the media,
* along with information about whether those ads have been loaded and played. * along with information about whether those ads have been loaded and played.
* <p> *
* The figure below shows some of the information defined by a period, as well as how this * <p>The figure below shows some of the information defined by a period, as well as how this
* information relates to a corresponding {@link Window} in the timeline. * information relates to a corresponding {@link Window} in the timeline.
* <p align="center"> *
* <img src="doc-files/timeline-period.svg" alt="Information defined by a period"> * <p style="align:center"><img src="doc-files/timeline-period.svg" alt="Information defined by a
* </p> * period">
*/ */
public static final class Period { public static final class Period {
......
...@@ -31,7 +31,7 @@ import java.nio.ByteBuffer; ...@@ -31,7 +31,7 @@ import java.nio.ByteBuffer;
/** /**
* Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the * Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the
* definition in ETSI TS 102 366 V1.2.1. * definition in ETSI TS 102 366 V1.4.1.
*/ */
public final class Ac3Util { public final class Ac3Util {
...@@ -39,8 +39,8 @@ public final class Ac3Util { ...@@ -39,8 +39,8 @@ public final class Ac3Util {
public static final class SyncFrameInfo { public static final class SyncFrameInfo {
/** /**
* AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, * AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link
* {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. * #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -114,9 +114,7 @@ public final class Ac3Util { ...@@ -114,9 +114,7 @@ public final class Ac3Util {
* The number of new samples per (E-)AC-3 audio block. * The number of new samples per (E-)AC-3 audio block.
*/ */
private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256; private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256;
/** /** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 4.1. */
* Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1.
*/
private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK; private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK;
/** /**
* Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod. * Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod.
...@@ -134,20 +132,21 @@ public final class Ac3Util { ...@@ -134,20 +132,21 @@ public final class Ac3Util {
* Channel counts, indexed by acmod. * Channel counts, indexed by acmod.
*/ */
private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5};
/** /** Nominal bitrates in kbps, indexed by frmsizecod / 2. (See table 4.13.) */
* Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) private static final int[] BITRATE_BY_HALF_FRMSIZECOD =
*/ new int[] {
private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640
112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640}; };
/** /** 16-bit words per syncframe, indexed by frmsizecod / 2. (See table 4.13.) */
* 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 =
*/ new int[] {
private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, 69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253,
121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; 1393
};
/** /**
* Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to Annex F.
* 102 366 Annex F. The reading position of {@code data} will be modified. * The reading position of {@code data} will be modified.
* *
* @param data The AC3SpecificBox to parse. * @param data The AC3SpecificBox to parse.
* @param trackId The track identifier to set on the format. * @param trackId The track identifier to set on the format.
...@@ -179,8 +178,8 @@ public final class Ac3Util { ...@@ -179,8 +178,8 @@ public final class Ac3Util {
} }
/** /**
* Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to ETSI TS * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to Annex
* 102 366 Annex F. The reading position of {@code data} will be modified. * F. The reading position of {@code data} will be modified.
* *
* @param data The EC3SpecificBox to parse. * @param data The EC3SpecificBox to parse.
* @param trackId The track identifier to set on the format. * @param trackId The track identifier to set on the format.
...@@ -243,9 +242,10 @@ public final class Ac3Util { ...@@ -243,9 +242,10 @@ public final class Ac3Util {
public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) {
int initialPosition = data.getPosition(); int initialPosition = data.getPosition();
data.skipBits(40); data.skipBits(40);
boolean isEac3 = data.readBits(5) == 16; // See bsid in subsection E.1.3.1.6. // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6).
boolean isEac3 = data.readBits(5) > 10;
data.setPosition(initialPosition); data.setPosition(initialPosition);
String mimeType; @Nullable String mimeType;
@StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED;
int sampleRate; int sampleRate;
int acmod; int acmod;
...@@ -254,7 +254,7 @@ public final class Ac3Util { ...@@ -254,7 +254,7 @@ public final class Ac3Util {
boolean lfeon; boolean lfeon;
int channelCount; int channelCount;
if (isEac3) { if (isEac3) {
// Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. // Subsection E.1.2.
data.skipBits(16); // syncword data.skipBits(16); // syncword
switch (data.readBits(2)) { // strmtyp switch (data.readBits(2)) { // strmtyp
case 0: case 0:
...@@ -472,7 +472,8 @@ public final class Ac3Util { ...@@ -472,7 +472,8 @@ public final class Ac3Util {
if (data.length < 6) { if (data.length < 6) {
return C.LENGTH_UNSET; return C.LENGTH_UNSET;
} }
boolean isEac3 = ((data[5] & 0xFF) >> 3) == 16; // See bsid in subsection E.1.3.1.6. // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6).
boolean isEac3 = ((data[5] & 0xF8) >> 3) > 10;
if (isEac3) { if (isEac3) {
int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits. int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits.
frmsiz |= data[3] & 0xFF; // Least significant 8 bits. frmsiz |= data[3] & 0xFF; // Least significant 8 bits.
...@@ -485,24 +486,22 @@ public final class Ac3Util { ...@@ -485,24 +486,22 @@ public final class Ac3Util {
} }
/** /**
* Returns the number of audio samples in an AC-3 syncframe. * Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's
*/
public static int getAc3SyncframeAudioSampleCount() {
return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;
}
/**
* Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's
* position is not modified. * position is not modified.
* *
* @param buffer The {@link ByteBuffer} from which to read the syncframe. * @param buffer The {@link ByteBuffer} from which to read the syncframe.
* @return The number of audio samples represented by the syncframe. * @return The number of audio samples represented by the syncframe.
*/ */
public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) {
// See ETSI TS 102 366 subsection E.1.2.2. // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6).
boolean isEac3 = ((buffer.get(buffer.position() + 5) & 0xF8) >> 3) > 10;
if (isEac3) {
int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6;
return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6 int numblkscod = fscod == 0x03 ? 3 : (buffer.get(buffer.position() + 4) & 0x30) >> 4;
: BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); return BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod] * AUDIO_SAMPLES_PER_AUDIO_BLOCK;
} else {
return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;
}
} }
/** /**
...@@ -535,8 +534,8 @@ public final class Ac3Util { ...@@ -535,8 +534,8 @@ public final class Ac3Util {
* contain the start of a syncframe. * contain the start of a syncframe.
*/ */
public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) { public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) {
// TODO: Link to specification if available. // See "Dolby TrueHD (MLP) high-level bitstream description" on the Dolby developer site,
// The syncword ends 0xBA for TrueHD or 0xBB for MLP. // subsections 2.2 and 4.2.1. The syncword ends 0xBA for TrueHD or 0xBB for MLP.
if (syncframe[4] != (byte) 0xF8 if (syncframe[4] != (byte) 0xF8
|| syncframe[5] != (byte) 0x72 || syncframe[5] != (byte) 0x72
|| syncframe[6] != (byte) 0x6F || syncframe[6] != (byte) 0x6F
......
...@@ -58,6 +58,11 @@ public final class Ac4Util { ...@@ -58,6 +58,11 @@ public final class Ac4Util {
// TODO: Parse AC-4 stream channel count. // TODO: Parse AC-4 stream channel count.
private static final int CHANNEL_COUNT_2 = 2; private static final int CHANNEL_COUNT_2 = 2;
/** /**
* The AC-4 sync frame header size for extractor. The seven bytes are 0xAC, 0x40, 0xFF, 0xFF,
* sizeByte1, sizeByte2, sizeByte3. See ETSI TS 103 190-1 V1.3.1, Annex G
*/
public static final int SAMPLE_HEADER_SIZE = 7;
/**
* The header size for AC-4 parser. Only needs to be as big as we need to read, not the full * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full
* header size. * header size.
*/ */
...@@ -218,7 +223,7 @@ public final class Ac4Util { ...@@ -218,7 +223,7 @@ public final class Ac4Util {
/** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */ /** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */
public static void getAc4SampleHeader(int size, ParsableByteArray buffer) { public static void getAc4SampleHeader(int size, ParsableByteArray buffer) {
// See ETSI TS 103 190-1 V1.3.1, Annex G. // See ETSI TS 103 190-1 V1.3.1, Annex G.
buffer.reset(/* limit= */ 7); buffer.reset(SAMPLE_HEADER_SIZE);
buffer.data[0] = (byte) 0xAC; buffer.data[0] = (byte) 0xAC;
buffer.data[1] = 0x40; buffer.data[1] = 0x40;
buffer.data[2] = (byte) 0xFF; buffer.data[2] = (byte) 0xFF;
......
...@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.C; ...@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException; import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -1148,9 +1149,7 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -1148,9 +1149,7 @@ public final class DefaultAudioSink implements AudioSink {
case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
case C.ENCODING_PCM_MU_LAW:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:
throw new IllegalArgumentException(); throw new IllegalArgumentException();
...@@ -1158,21 +1157,25 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -1158,21 +1157,25 @@ public final class DefaultAudioSink implements AudioSink {
} }
private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) { switch (encoding) {
case C.ENCODING_MP3:
return MpegAudioHeader.getFrameSampleCount(buffer.get(buffer.position()));
case C.ENCODING_DTS:
case C.ENCODING_DTS_HD:
return DtsUtil.parseDtsAudioSampleCount(buffer); return DtsUtil.parseDtsAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_AC3) { case C.ENCODING_AC3:
return Ac3Util.getAc3SyncframeAudioSampleCount(); case C.ENCODING_E_AC3:
} else if (encoding == C.ENCODING_E_AC3 || encoding == C.ENCODING_E_AC3_JOC) { case C.ENCODING_E_AC3_JOC:
return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_AC4) { case C.ENCODING_AC4:
return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_DOLBY_TRUEHD) { case C.ENCODING_DOLBY_TRUEHD:
int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer);
return syncframeOffset == C.INDEX_UNSET return syncframeOffset == C.INDEX_UNSET
? 0 ? 0
: (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset)
* Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT);
} else { default:
throw new IllegalStateException("Unexpected audio encoding: " + encoding); throw new IllegalStateException("Unexpected audio encoding: " + encoding);
} }
} }
......
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.audio;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.PlaybackParameters;
import java.nio.ByteBuffer;
/** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */
public class ForwardingAudioSink implements AudioSink {
private final AudioSink sink;
public ForwardingAudioSink(AudioSink sink) {
this.sink = sink;
}
@Override
public void setListener(Listener listener) {
sink.setListener(listener);
}
@Override
public boolean supportsOutput(int channelCount, int encoding) {
return sink.supportsOutput(channelCount, encoding);
}
@Override
public long getCurrentPositionUs(boolean sourceEnded) {
return sink.getCurrentPositionUs(sourceEnded);
}
@Override
public void configure(
int inputEncoding,
int inputChannelCount,
int inputSampleRate,
int specifiedBufferSize,
@Nullable int[] outputChannels,
int trimStartFrames,
int trimEndFrames)
throws ConfigurationException {
sink.configure(
inputEncoding,
inputChannelCount,
inputSampleRate,
specifiedBufferSize,
outputChannels,
trimStartFrames,
trimEndFrames);
}
@Override
public void play() {
sink.play();
}
@Override
public void handleDiscontinuity() {
sink.handleDiscontinuity();
}
@Override
public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
throws InitializationException, WriteException {
return sink.handleBuffer(buffer, presentationTimeUs);
}
@Override
public void playToEndOfStream() throws WriteException {
sink.playToEndOfStream();
}
@Override
public boolean isEnded() {
return sink.isEnded();
}
@Override
public boolean hasPendingData() {
return sink.hasPendingData();
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
sink.setPlaybackParameters(playbackParameters);
}
@Override
public PlaybackParameters getPlaybackParameters() {
return sink.getPlaybackParameters();
}
@Override
public void setAudioAttributes(AudioAttributes audioAttributes) {
sink.setAudioAttributes(audioAttributes);
}
@Override
public void setAudioSessionId(int audioSessionId) {
sink.setAudioSessionId(audioSessionId);
}
@Override
public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) {
sink.setAuxEffectInfo(auxEffectInfo);
}
@Override
public void enableTunnelingV21(int tunnelingAudioSessionId) {
sink.enableTunnelingV21(tunnelingAudioSessionId);
}
@Override
public void disableTunneling() {
sink.disableTunneling();
}
@Override
public void setVolume(float volume) {
sink.setVolume(volume);
}
@Override
public void pause() {
sink.pause();
}
@Override
public void flush() {
sink.flush();
}
@Override
public void reset() {
sink.reset();
}
}
...@@ -79,6 +79,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -79,6 +79,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10; private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10;
private static final String TAG = "MediaCodecAudioRenderer"; private static final String TAG = "MediaCodecAudioRenderer";
/**
* Custom key used to indicate bits per sample by some decoders on Vivo devices. For example
* OMX.vivo.alac.decoder on the Vivo Z1 Pro.
*/
private static final String VIVO_BITS_PER_SAMPLE_KEY = "v-bits-per-sample";
private final Context context; private final Context context;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
...@@ -566,8 +571,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -566,8 +571,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
mediaFormat.getString(MediaFormat.KEY_MIME)); mediaFormat.getString(MediaFormat.KEY_MIME));
} else { } else {
mediaFormat = outputMediaFormat; mediaFormat = outputMediaFormat;
if (outputMediaFormat.containsKey(VIVO_BITS_PER_SAMPLE_KEY)) {
encoding = Util.getPcmEncoding(outputMediaFormat.getInteger(VIVO_BITS_PER_SAMPLE_KEY));
} else {
encoding = getPcmEncoding(inputFormat); encoding = getPcmEncoding(inputFormat);
} }
}
int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int[] channelMap; int[] channelMap;
......
...@@ -29,8 +29,11 @@ import java.nio.ByteBuffer; ...@@ -29,8 +29,11 @@ import java.nio.ByteBuffer;
public AudioFormat onConfigure(AudioFormat inputAudioFormat) public AudioFormat onConfigure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException { throws UnhandledAudioFormatException {
@C.PcmEncoding int encoding = inputAudioFormat.encoding; @C.PcmEncoding int encoding = inputAudioFormat.encoding;
if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT if (encoding != C.ENCODING_PCM_8BIT
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { && encoding != C.ENCODING_PCM_16BIT
&& encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN
&& encoding != C.ENCODING_PCM_24BIT
&& encoding != C.ENCODING_PCM_32BIT) {
throw new UnhandledAudioFormatException(inputAudioFormat); throw new UnhandledAudioFormatException(inputAudioFormat);
} }
return encoding != C.ENCODING_PCM_16BIT return encoding != C.ENCODING_PCM_16BIT
...@@ -50,6 +53,9 @@ import java.nio.ByteBuffer; ...@@ -50,6 +53,9 @@ import java.nio.ByteBuffer;
case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_8BIT:
resampledSize = size * 2; resampledSize = size * 2;
break; break;
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
resampledSize = size;
break;
case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_24BIT:
resampledSize = (size / 3) * 2; resampledSize = (size / 3) * 2;
break; break;
...@@ -58,8 +64,6 @@ import java.nio.ByteBuffer; ...@@ -58,8 +64,6 @@ import java.nio.ByteBuffer;
break; break;
case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_MU_LAW:
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:
...@@ -70,21 +74,28 @@ import java.nio.ByteBuffer; ...@@ -70,21 +74,28 @@ import java.nio.ByteBuffer;
ByteBuffer buffer = replaceOutputBuffer(resampledSize); ByteBuffer buffer = replaceOutputBuffer(resampledSize);
switch (inputAudioFormat.encoding) { switch (inputAudioFormat.encoding) {
case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_8BIT:
// 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. // 8 -> 16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
for (int i = position; i < limit; i++) { for (int i = position; i < limit; i++) {
buffer.put((byte) 0); buffer.put((byte) 0);
buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128));
} }
break; break;
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
// Big endian to little endian resampling. Swap the byte order.
for (int i = position; i < limit; i += 2) {
buffer.put(inputBuffer.get(i + 1));
buffer.put(inputBuffer.get(i));
}
break;
case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_24BIT:
// 24->16 bit resampling. Drop the least significant byte. // 24 -> 16 bit resampling. Drop the least significant byte.
for (int i = position; i < limit; i += 3) { for (int i = position; i < limit; i += 3) {
buffer.put(inputBuffer.get(i + 1)); buffer.put(inputBuffer.get(i + 1));
buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 2));
} }
break; break;
case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_32BIT:
// 32->16 bit resampling. Drop the two least significant bytes. // 32 -> 16 bit resampling. Drop the two least significant bytes.
for (int i = position; i < limit; i += 4) { for (int i = position; i < limit; i += 4) {
buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 2));
buffer.put(inputBuffer.get(i + 3)); buffer.put(inputBuffer.get(i + 3));
...@@ -92,8 +103,6 @@ import java.nio.ByteBuffer; ...@@ -92,8 +103,6 @@ import java.nio.ByteBuffer;
break; break;
case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_MU_LAW:
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:
......
...@@ -97,6 +97,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -97,6 +97,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private final AudioSink audioSink; private final AudioSink audioSink;
private final DecoderInputBuffer flagsOnlyBuffer; private final DecoderInputBuffer flagsOnlyBuffer;
private boolean drmResourcesAcquired;
private DecoderCounters decoderCounters; private DecoderCounters decoderCounters;
private Format inputFormat; private Format inputFormat;
private int encoderDelay; private int encoderDelay;
...@@ -351,15 +352,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -351,15 +352,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
/** /**
* Returns the format of audio buffers output by the decoder. Will not be called until the first * Returns the format of audio buffers output by the decoder. Will not be called until the first
* output buffer has been dequeued, so the decoder may use input data to determine the format. * output buffer has been dequeued, so the decoder may use input data to determine the format.
* <p>
* The default implementation returns a 16-bit PCM format with the same channel count and sample
* rate as the input.
*/ */
protected Format getOutputFormat() { protected abstract Format getOutputFormat();
return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
Format.NO_VALUE, inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT,
null, null, 0, null);
}
/** /**
* Returns whether the existing decoder can be kept for a new format. * Returns whether the existing decoder can be kept for a new format.
...@@ -546,6 +540,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -546,6 +540,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override @Override
protected void onEnabled(boolean joining) throws ExoPlaybackException { protected void onEnabled(boolean joining) throws ExoPlaybackException {
if (drmSessionManager != null && !drmResourcesAcquired) {
drmResourcesAcquired = true;
drmSessionManager.prepare();
}
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
...@@ -595,6 +593,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -595,6 +593,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} }
@Override @Override
protected void onReset() {
if (drmSessionManager != null && drmResourcesAcquired) {
drmResourcesAcquired = false;
drmSessionManager.release();
}
}
@Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
switch (messageType) { switch (messageType) {
case C.MSG_SET_VOLUME: case C.MSG_SET_VOLUME:
......
...@@ -176,7 +176,7 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { ...@@ -176,7 +176,7 @@ public final class TeeAudioProcessor extends BaseAudioProcessor {
// Write the rest of the header as little endian data. // Write the rest of the header as little endian data.
scratchByteBuffer.clear(); scratchByteBuffer.clear();
scratchByteBuffer.putInt(16); scratchByteBuffer.putInt(16);
scratchByteBuffer.putShort((short) WavUtil.getTypeForEncoding(encoding)); scratchByteBuffer.putShort((short) WavUtil.getTypeForPcmEncoding(encoding));
scratchByteBuffer.putShort((short) channelCount); scratchByteBuffer.putShort((short) channelCount);
scratchByteBuffer.putInt(sampleRateHz); scratchByteBuffer.putInt(sampleRateHz);
int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount); int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount);
......
...@@ -32,28 +32,33 @@ public final class WavUtil { ...@@ -32,28 +32,33 @@ public final class WavUtil {
public static final int DATA_FOURCC = 0x64617461; public static final int DATA_FOURCC = 0x64617461;
/** WAVE type value for integer PCM audio data. */ /** WAVE type value for integer PCM audio data. */
private static final int TYPE_PCM = 0x0001; public static final int TYPE_PCM = 0x0001;
/** WAVE type value for float PCM audio data. */ /** WAVE type value for float PCM audio data. */
private static final int TYPE_FLOAT = 0x0003; public static final int TYPE_FLOAT = 0x0003;
/** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */
private static final int TYPE_A_LAW = 0x0006; public static final int TYPE_ALAW = 0x0006;
/** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */
private static final int TYPE_MU_LAW = 0x0007; public static final int TYPE_MLAW = 0x0007;
/** WAVE type value for IMA ADPCM audio data. */
public static final int TYPE_IMA_ADPCM = 0x0011;
/** WAVE type value for extended WAVE format. */ /** WAVE type value for extended WAVE format. */
private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; public static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
/** Returns the WAVE type value for the given {@code encoding}. */ /**
public static int getTypeForEncoding(@C.PcmEncoding int encoding) { * Returns the WAVE format type value for the given {@link C.PcmEncoding}.
switch (encoding) { *
* @param pcmEncoding The {@link C.PcmEncoding} value.
* @return The corresponding WAVE format type.
* @throws IllegalArgumentException If {@code pcmEncoding} is not a {@link C.PcmEncoding}, or if
* it's {@link C#ENCODING_INVALID} or {@link Format#NO_VALUE}.
*/
public static int getTypeForPcmEncoding(@C.PcmEncoding int pcmEncoding) {
switch (pcmEncoding) {
case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_32BIT:
return TYPE_PCM; return TYPE_PCM;
case C.ENCODING_PCM_A_LAW:
return TYPE_A_LAW;
case C.ENCODING_PCM_MU_LAW:
return TYPE_MU_LAW;
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
return TYPE_FLOAT; return TYPE_FLOAT;
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
...@@ -63,18 +68,17 @@ public final class WavUtil { ...@@ -63,18 +68,17 @@ public final class WavUtil {
} }
} }
/** Returns the PCM encoding for the given WAVE {@code type} value. */ /**
public static @C.PcmEncoding int getEncodingForType(int type, int bitsPerSample) { * Returns the {@link C.PcmEncoding} for the given WAVE format type value, or {@link
* C#ENCODING_INVALID} if the type is not a known PCM type.
*/
public static @C.PcmEncoding int getPcmEncodingForType(int type, int bitsPerSample) {
switch (type) { switch (type) {
case TYPE_PCM: case TYPE_PCM:
case TYPE_WAVE_FORMAT_EXTENSIBLE: case TYPE_WAVE_FORMAT_EXTENSIBLE:
return Util.getPcmEncoding(bitsPerSample); return Util.getPcmEncoding(bitsPerSample);
case TYPE_FLOAT: case TYPE_FLOAT:
return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID;
case TYPE_A_LAW:
return C.ENCODING_PCM_A_LAW;
case TYPE_MU_LAW:
return C.ENCODING_PCM_MU_LAW;
default: default:
return C.ENCODING_INVALID; return C.ENCODING_INVALID;
} }
......
...@@ -24,7 +24,6 @@ import java.io.IOException; ...@@ -24,7 +24,6 @@ import java.io.IOException;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
/** /**
* A seeker that supports seeking within a stream by searching for the target frame using binary * A seeker that supports seeking within a stream by searching for the target frame using binary
...@@ -48,15 +47,11 @@ public abstract class BinarySearchSeeker { ...@@ -48,15 +47,11 @@ public abstract class BinarySearchSeeker {
* *
* @param input The {@link ExtractorInput} from which data should be peeked. * @param input The {@link ExtractorInput} from which data should be peeked.
* @param targetTimestamp The target timestamp. * @param targetTimestamp The target timestamp.
* @param outputFrameHolder If {@link TimestampSearchResult#TYPE_TARGET_TIMESTAMP_FOUND} is
* returned, this holder may be updated to hold the extracted frame that contains the target
* frame/sample associated with the target timestamp.
* @return A {@link TimestampSearchResult} that describes the result of the search. * @return A {@link TimestampSearchResult} that describes the result of the search.
* @throws IOException If an error occurred reading from the input. * @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted. * @throws InterruptedException If the thread was interrupted.
*/ */
TimestampSearchResult searchForTimestamp( TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
throws IOException, InterruptedException; throws IOException, InterruptedException;
/** Called when a seek operation finishes. */ /** Called when a seek operation finishes. */
...@@ -64,23 +59,6 @@ public abstract class BinarySearchSeeker { ...@@ -64,23 +59,6 @@ public abstract class BinarySearchSeeker {
} }
/** /**
* Holds a frame extracted from a stream, together with the time stamp of the frame in
* microseconds.
*/
public static final class OutputFrameHolder {
public final ByteBuffer byteBuffer;
public long timeUs;
/** Constructs an instance, wrapping the given byte buffer. */
public OutputFrameHolder(ByteBuffer outputByteBuffer) {
this.timeUs = 0;
this.byteBuffer = outputByteBuffer;
}
}
/**
* A {@link SeekTimestampConverter} implementation that returns the seek time itself as the * A {@link SeekTimestampConverter} implementation that returns the seek time itself as the
* timestamp for a seek time position. * timestamp for a seek time position.
*/ */
...@@ -189,15 +167,11 @@ public abstract class BinarySearchSeeker { ...@@ -189,15 +167,11 @@ public abstract class BinarySearchSeeker {
* @param input The {@link ExtractorInput} from which data should be read. * @param input The {@link ExtractorInput} from which data should be read.
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
* to hold the position of the required seek. * to hold the position of the required seek.
* @param outputFrameHolder If {@link Extractor#RESULT_CONTINUE} is returned, this holder may be
* updated to hold the extracted frame that contains the target sample. The caller needs to
* check the byte buffer limit to see if an extracted frame is available.
* @return One of the {@code RESULT_} values defined in {@link Extractor}. * @return One of the {@code RESULT_} values defined in {@link Extractor}.
* @throws IOException If an error occurred reading from the input. * @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted. * @throws InterruptedException If the thread was interrupted.
*/ */
public int handlePendingSeek( public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder)
ExtractorInput input, PositionHolder seekPositionHolder, OutputFrameHolder outputFrameHolder)
throws InterruptedException, IOException { throws InterruptedException, IOException {
TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker); TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker);
while (true) { while (true) {
...@@ -217,8 +191,7 @@ public abstract class BinarySearchSeeker { ...@@ -217,8 +191,7 @@ public abstract class BinarySearchSeeker {
input.resetPeekPosition(); input.resetPeekPosition();
TimestampSearchResult timestampSearchResult = TimestampSearchResult timestampSearchResult =
timestampSeeker.searchForTimestamp( timestampSeeker.searchForTimestamp(input, seekOperationParams.getTargetTimePosition());
input, seekOperationParams.getTargetTimePosition(), outputFrameHolder);
switch (timestampSearchResult.type) { switch (timestampSearchResult.type) {
case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED: case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED:
...@@ -419,7 +392,7 @@ public abstract class BinarySearchSeeker { ...@@ -419,7 +392,7 @@ public abstract class BinarySearchSeeker {
/** /**
* Represents possible search results for {@link * Represents possible search results for {@link
* TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}. * TimestampSeeker#searchForTimestamp(ExtractorInput, long)}.
*/ */
public static final class TimestampSearchResult { public static final class TimestampSearchResult {
...@@ -495,10 +468,6 @@ public abstract class BinarySearchSeeker { ...@@ -495,10 +468,6 @@ public abstract class BinarySearchSeeker {
/** /**
* Returns a result to signal that the target timestamp has been found at {@code * Returns a result to signal that the target timestamp has been found at {@code
* resultBytePosition}, and the seek operation can stop. * resultBytePosition}, and the seek operation can stop.
*
* <p>Note that when this value is returned from {@link
* TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}, the {@link
* OutputFrameHolder} may be updated to hold the target frame as an optimization.
*/ */
public static TimestampSearchResult targetFoundResult(long resultBytePosition) { public static TimestampSearchResult targetFoundResult(long resultBytePosition) {
return new TimestampSearchResult( return new TimestampSearchResult(
......
...@@ -58,7 +58,9 @@ public final class DefaultExtractorInput implements ExtractorInput { ...@@ -58,7 +58,9 @@ public final class DefaultExtractorInput implements ExtractorInput {
public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {
int bytesRead = readFromPeekBuffer(target, offset, length); int bytesRead = readFromPeekBuffer(target, offset, length);
if (bytesRead == 0) { if (bytesRead == 0) {
bytesRead = readFromDataSource(target, offset, length, 0, true); bytesRead =
readFromDataSource(
target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true);
} }
commitBytesRead(bytesRead); commitBytesRead(bytesRead);
return bytesRead; return bytesRead;
...@@ -111,6 +113,31 @@ public final class DefaultExtractorInput implements ExtractorInput { ...@@ -111,6 +113,31 @@ public final class DefaultExtractorInput implements ExtractorInput {
} }
@Override @Override
public int peek(byte[] target, int offset, int length) throws IOException, InterruptedException {
ensureSpaceForPeek(length);
int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition;
int bytesPeeked;
if (peekBufferRemainingBytes == 0) {
bytesPeeked =
readFromDataSource(
peekBuffer,
peekBufferPosition,
length,
/* bytesAlreadyRead= */ 0,
/* allowEndOfInput= */ true);
if (bytesPeeked == C.RESULT_END_OF_INPUT) {
return C.RESULT_END_OF_INPUT;
}
peekBufferLength += bytesPeeked;
} else {
bytesPeeked = Math.min(length, peekBufferRemainingBytes);
}
System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked);
peekBufferPosition += bytesPeeked;
return bytesPeeked;
}
@Override
public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException, InterruptedException { throws IOException, InterruptedException {
if (!advancePeekPosition(length, allowEndOfInput)) { if (!advancePeekPosition(length, allowEndOfInput)) {
...@@ -201,7 +228,7 @@ public final class DefaultExtractorInput implements ExtractorInput { ...@@ -201,7 +228,7 @@ public final class DefaultExtractorInput implements ExtractorInput {
} }
/** /**
* Reads from the peek buffer * Reads from the peek buffer.
* *
* @param target A target array into which data should be written. * @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write. * @param offset The offset into the target array at which to write.
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor; package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.extractor.amr.AmrExtractor; import com.google.android.exoplayer2.extractor.amr.AmrExtractor;
import com.google.android.exoplayer2.extractor.flac.FlacExtractor;
import com.google.android.exoplayer2.extractor.flv.FlvExtractor; import com.google.android.exoplayer2.extractor.flv.FlvExtractor;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
...@@ -55,15 +56,24 @@ import java.lang.reflect.Constructor; ...@@ -55,15 +56,24 @@ import java.lang.reflect.Constructor;
*/ */
public final class DefaultExtractorsFactory implements ExtractorsFactory { public final class DefaultExtractorsFactory implements ExtractorsFactory {
private static final Constructor<? extends Extractor> FLAC_EXTRACTOR_CONSTRUCTOR; private static final Constructor<? extends Extractor> FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR;
static { static {
Constructor<? extends Extractor> flacExtractorConstructor = null; Constructor<? extends Extractor> flacExtensionExtractorConstructor = null;
try { try {
// LINT.IfChange // LINT.IfChange
flacExtractorConstructor = @SuppressWarnings("nullness:argument.type.incompatible")
boolean isFlacNativeLibraryAvailable =
Boolean.TRUE.equals(
Class.forName("com.google.android.exoplayer2.ext.flac.FlacLibrary")
.getMethod("isAvailable")
.invoke(/* obj= */ null));
if (isFlacNativeLibraryAvailable) {
flacExtensionExtractorConstructor =
Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor")
.asSubclass(Extractor.class) .asSubclass(Extractor.class)
.getConstructor(); .getConstructor();
}
// LINT.ThenChange(../../../../../../../../proguard-rules.txt) // LINT.ThenChange(../../../../../../../../proguard-rules.txt)
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
// Expected if the app was built without the FLAC extension. // Expected if the app was built without the FLAC extension.
...@@ -71,7 +81,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { ...@@ -71,7 +81,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
// The FLAC extension is present, but instantiation failed. // The FLAC extension is present, but instantiation failed.
throw new RuntimeException("Error instantiating FLAC extension", e); throw new RuntimeException("Error instantiating FLAC extension", e);
} }
FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor; FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR = flacExtensionExtractorConstructor;
} }
private boolean constantBitrateSeekingEnabled; private boolean constantBitrateSeekingEnabled;
...@@ -208,7 +218,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { ...@@ -208,7 +218,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Override @Override
public synchronized Extractor[] createExtractors() { public synchronized Extractor[] createExtractors() {
Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 13 : 14]; Extractor[] extractors = new Extractor[14];
extractors[0] = new MatroskaExtractor(matroskaFlags); extractors[0] = new MatroskaExtractor(matroskaFlags);
extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);
extractors[2] = new Mp4Extractor(mp4Flags); extractors[2] = new Mp4Extractor(mp4Flags);
...@@ -237,13 +247,19 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { ...@@ -237,13 +247,19 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0)); : 0));
extractors[12] = new Ac4Extractor(); extractors[12] = new Ac4Extractor();
if (FLAC_EXTRACTOR_CONSTRUCTOR != null) { // Prefer the FLAC extension extractor because it outputs raw audio, which can be handled by the
// framework on all API levels, unlike the core library FLAC extractor, which outputs FLAC audio
// frames and so relies on having a FLAC decoder (e.g., a MediaCodec decoder that handles FLAC
// (from API 27), or the FFmpeg extension with FLAC enabled).
if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) {
try { try {
extractors[13] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance();
} catch (Exception e) { } catch (Exception e) {
// Should never happen. // Should never happen.
throw new IllegalStateException("Unexpected error creating FLAC extractor", e); throw new IllegalStateException("Unexpected error creating FLAC extractor", e);
} }
} else {
extractors[13] = new FlacExtractor();
} }
return extractors; return extractors;
} }
......
...@@ -27,19 +27,19 @@ import java.io.InputStream; ...@@ -27,19 +27,19 @@ import java.io.InputStream;
* for more info about each mode. * for more info about each mode.
* *
* <ul> * <ul>
* <li>The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level * <li>The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like
* access operations. * byte-level access operations.
* <li>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user * <li>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user
* wants to read an entire block/frame/header of known length. * wants to read an entire block/frame/header of known length.
* </ul> * </ul>
* *
* <h3>{@link InputStream}-like methods</h3> * <h3>{@link InputStream}-like methods</h3>
* *
* <p>The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level * <p>The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like
* access operations. The {@code length} parameter is a maximum, and each method returns the number * byte-level access operations. The {@code length} parameter is a maximum, and each method returns
* of bytes actually processed. This may be less than {@code length} because the end of the input * the number of bytes actually processed. This may be less than {@code length} because the end of
* was reached, or the method was interrupted, or the operation was aborted early for another * the input was reached, or the method was interrupted, or the operation was aborted early for
* reason. * another reason.
* *
* <h3>Block-based methods</h3> * <h3>Block-based methods</h3>
* *
...@@ -102,7 +102,8 @@ public interface ExtractorInput { ...@@ -102,7 +102,8 @@ public interface ExtractorInput {
throws IOException, InterruptedException; throws IOException, InterruptedException;
/** /**
* Equivalent to {@code readFully(target, offset, length, false)}. * Equivalent to {@link #readFully(byte[], int, int, boolean) readFully(target, offset, length,
* false)}.
* *
* @param target A target array into which data should be written. * @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write. * @param offset The offset into the target array at which to write.
...@@ -155,8 +156,11 @@ public interface ExtractorInput { ...@@ -155,8 +156,11 @@ public interface ExtractorInput {
void skipFully(int length) throws IOException, InterruptedException; void skipFully(int length) throws IOException, InterruptedException;
/** /**
* Peeks {@code length} bytes from the peek position, writing them into {@code target} at index * Peeks up to {@code length} bytes from the peek position. The current read position is left
* {@code offset}. The current read position is left unchanged. * unchanged.
*
* <p>This method blocks until at least one byte of data can be peeked, the end of the input is
* detected, or an exception is thrown.
* *
* <p>Calling {@link #resetPeekPosition()} resets the peek position to equal the current read * <p>Calling {@link #resetPeekPosition()} resets the peek position to equal the current read
* position, so the caller can peek the same data again. Reading or skipping also resets the peek * position, so the caller can peek the same data again. Reading or skipping also resets the peek
...@@ -164,6 +168,18 @@ public interface ExtractorInput { ...@@ -164,6 +168,18 @@ public interface ExtractorInput {
* *
* @param target A target array into which data should be written. * @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write. * @param offset The offset into the target array at which to write.
* @param length The maximum number of bytes to peek from the input.
* @return The number of bytes peeked, or {@link C#RESULT_END_OF_INPUT} if the input has ended.
* @throws IOException If an error occurs peeking from the input.
* @throws InterruptedException If the thread has been interrupted.
*/
int peek(byte[] target, int offset, int length) throws IOException, InterruptedException;
/**
* Like {@link #peek(byte[], int, int)}, but peeks the requested {@code length} in full.
*
* @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write.
* @param length The number of bytes to peek from the input. * @param length The number of bytes to peek from the input.
* @param allowEndOfInput True if encountering the end of the input having peeked no data is * @param allowEndOfInput True if encountering the end of the input having peeked no data is
* allowed, and should result in {@code false} being returned. False if it should be * allowed, and should result in {@code false} being returned. False if it should be
...@@ -181,12 +197,8 @@ public interface ExtractorInput { ...@@ -181,12 +197,8 @@ public interface ExtractorInput {
throws IOException, InterruptedException; throws IOException, InterruptedException;
/** /**
* Peeks {@code length} bytes from the peek position, writing them into {@code target} at index * Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(target, offset, length,
* {@code offset}. The current read position is left unchanged. * false)}.
* <p>
* Calling {@link #resetPeekPosition()} resets the peek position to equal the current read
* position, so the caller can peek the same data again. Reading and skipping also reset the peek
* position.
* *
* @param target A target array into which data should be written. * @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write. * @param offset The offset into the target array at which to write.
......
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.C;
import java.io.IOException;
/** Extractor related utility methods. */
/* package */ final class ExtractorUtil {
/**
* Peeks {@code length} bytes from the input peek position, or all the bytes to the end of the
* input if there was less than {@code length} bytes left.
*
* <p>If an exception is thrown, there is no guarantee on the peek position.
*
* @param input The stream input to peek the data from.
* @param target A target array into which data should be written.
* @param offset The offset into the target array at which to write.
* @param length The maximum number of bytes to peek from the input.
* @return The number of bytes peeked.
* @throws IOException If an error occurs peeking from the input.
* @throws InterruptedException If the thread has been interrupted.
*/
public static int peekToLength(ExtractorInput input, byte[] target, int offset, int length)
throws IOException, InterruptedException {
int totalBytesPeeked = 0;
while (totalBytesPeeked < length) {
int bytesPeeked = input.peek(target, offset + totalBytesPeeked, length - totalBytesPeeked);
if (bytesPeeked == C.RESULT_END_OF_INPUT) {
break;
}
totalBytesPeeked += bytesPeeked;
}
return totalBytesPeeked;
}
private ExtractorUtil() {}
}
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.Util;
/**
* A {@link SeekMap} implementation for FLAC streams that contain a <a
* href="https://xiph.org/flac/format.html#metadata_block_seektable">seek table</a>.
*/
public final class FlacSeekTableSeekMap implements SeekMap {
private final FlacStreamMetadata flacStreamMetadata;
private final long firstFrameOffset;
/**
* Creates a seek map from the FLAC stream seek table.
*
* @param flacStreamMetadata The stream metadata.
* @param firstFrameOffset The byte offset of the first frame in the stream.
*/
public FlacSeekTableSeekMap(FlacStreamMetadata flacStreamMetadata, long firstFrameOffset) {
this.flacStreamMetadata = flacStreamMetadata;
this.firstFrameOffset = firstFrameOffset;
}
@Override
public boolean isSeekable() {
return true;
}
@Override
public long getDurationUs() {
return flacStreamMetadata.getDurationUs();
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
Assertions.checkNotNull(flacStreamMetadata.seekTable);
long[] pointSampleNumbers = flacStreamMetadata.seekTable.pointSampleNumbers;
long[] pointOffsets = flacStreamMetadata.seekTable.pointOffsets;
long targetSampleNumber = flacStreamMetadata.getSampleNumber(timeUs);
int index =
Util.binarySearchFloor(
pointSampleNumbers,
targetSampleNumber,
/* inclusive= */ true,
/* stayInBounds= */ false);
long seekPointSampleNumber = index == -1 ? 0 : pointSampleNumbers[index];
long seekPointOffsetFromFirstFrame = index == -1 ? 0 : pointOffsets[index];
SeekPoint seekPoint = getSeekPoint(seekPointSampleNumber, seekPointOffsetFromFirstFrame);
if (seekPoint.timeUs == timeUs || index == pointSampleNumbers.length - 1) {
return new SeekPoints(seekPoint);
} else {
SeekPoint secondSeekPoint =
getSeekPoint(pointSampleNumbers[index + 1], pointOffsets[index + 1]);
return new SeekPoints(seekPoint, secondSeekPoint);
}
}
private SeekPoint getSeekPoint(long sampleNumber, long offsetFromFirstFrame) {
long seekTimeUs = sampleNumber * C.MICROS_PER_SECOND / flacStreamMetadata.sampleRate;
long seekPosition = firstFrameOffset + offsetFromFirstFrame;
return new SeekPoint(seekTimeUs, seekPosition);
}
}
...@@ -56,12 +56,17 @@ public final class MpegAudioHeader { ...@@ -56,12 +56,17 @@ public final class MpegAudioHeader {
160000 160000
}; };
private static final int SAMPLES_PER_FRAME_L1 = 384;
private static final int SAMPLES_PER_FRAME_L2 = 1152;
private static final int SAMPLES_PER_FRAME_L3_V1 = 1152;
private static final int SAMPLES_PER_FRAME_L3_V2 = 576;
/** /**
* Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it
* is invalid. * is invalid.
*/ */
public static int getFrameSize(int header) { public static int getFrameSize(int header) {
if ((header & 0xFFE00000) != 0xFFE00000) { if (!isMagicPresent(header)) {
return C.LENGTH_UNSET; return C.LENGTH_UNSET;
} }
...@@ -121,6 +126,36 @@ public final class MpegAudioHeader { ...@@ -121,6 +126,36 @@ public final class MpegAudioHeader {
} }
/** /**
* Returns the number of samples per frame associated with {@code header}, or {@link
* C#LENGTH_UNSET} if it is invalid.
*/
public static int getFrameSampleCount(int header) {
if (!isMagicPresent(header)) {
return C.LENGTH_UNSET;
}
int version = (header >>> 19) & 3;
if (version == 1) {
return C.LENGTH_UNSET;
}
int layer = (header >>> 17) & 3;
if (layer == 0) {
return C.LENGTH_UNSET;
}
// Those header values are not used but are checked for consistency with the other methods
int bitrateIndex = (header >>> 12) & 15;
int samplingRateIndex = (header >>> 10) & 3;
if (bitrateIndex == 0 || bitrateIndex == 0xF || samplingRateIndex == 3) {
return C.LENGTH_UNSET;
}
return getFrameSizeInSamples(version, layer);
}
/**
* Parses {@code headerData}, populating {@code header} with the parsed data. * Parses {@code headerData}, populating {@code header} with the parsed data.
* *
* @param headerData Header data to parse. * @param headerData Header data to parse.
...@@ -129,7 +164,7 @@ public final class MpegAudioHeader { ...@@ -129,7 +164,7 @@ public final class MpegAudioHeader {
* is not a valid MPEG audio header. * is not a valid MPEG audio header.
*/ */
public static boolean populateHeader(int headerData, MpegAudioHeader header) { public static boolean populateHeader(int headerData, MpegAudioHeader header) {
if ((headerData & 0xFFE00000) != 0xFFE00000) { if (!isMagicPresent(headerData)) {
return false; return false;
} }
...@@ -166,23 +201,20 @@ public final class MpegAudioHeader { ...@@ -166,23 +201,20 @@ public final class MpegAudioHeader {
int padding = (headerData >>> 9) & 1; int padding = (headerData >>> 9) & 1;
int bitrate; int bitrate;
int frameSize; int frameSize;
int samplesPerFrame; int samplesPerFrame = getFrameSizeInSamples(version, layer);
if (layer == 3) { if (layer == 3) {
// Layer I (layer == 3) // Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
frameSize = (12 * bitrate / sampleRate + padding) * 4; frameSize = (12 * bitrate / sampleRate + padding) * 4;
samplesPerFrame = 384;
} else { } else {
// Layer II (layer == 2) or III (layer == 1) // Layer II (layer == 2) or III (layer == 1)
if (version == 3) { if (version == 3) {
// Version 1 // Version 1
bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
samplesPerFrame = 1152;
frameSize = 144 * bitrate / sampleRate + padding; frameSize = 144 * bitrate / sampleRate + padding;
} else { } else {
// Version 2 or 2.5. // Version 2 or 2.5.
bitrate = BITRATE_V2[bitrateIndex - 1]; bitrate = BITRATE_V2[bitrateIndex - 1];
samplesPerFrame = layer == 1 ? 576 : 1152;
frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding; frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding;
} }
} }
...@@ -193,6 +225,22 @@ public final class MpegAudioHeader { ...@@ -193,6 +225,22 @@ public final class MpegAudioHeader {
return true; return true;
} }
private static boolean isMagicPresent(int header) {
return (header & 0xFFE00000) == 0xFFE00000;
}
private static int getFrameSizeInSamples(int version, int layer) {
switch (layer) {
case 1:
return version == 3 ? SAMPLES_PER_FRAME_L3_V1 : SAMPLES_PER_FRAME_L3_V2; // Layer III
case 2:
return SAMPLES_PER_FRAME_L2; // Layer II
case 3:
return SAMPLES_PER_FRAME_L1; // Layer I
}
throw new IllegalArgumentException();
}
/** MPEG audio header version. */ /** MPEG audio header version. */
public int version; public int version;
/** The mime type. */ /** The mime type. */
......
...@@ -13,17 +13,17 @@ ...@@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
/** /**
* Wraps a byte array, providing methods that allow it to be read as a vorbis bitstream. * Wraps a byte array, providing methods that allow it to be read as a Vorbis bitstream.
* *
* @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking * @see <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-360002">Vorbis bitpacking
* specification</a> * specification</a>
*/ */
/* package */ final class VorbisBitArray { public final class VorbisBitArray {
private final byte[] data; private final byte[] data;
private final int byteLimit; private final int byteLimit;
......
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.flac;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.FlacFrameReader;
import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.FlacConstants;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
/**
* A {@link SeekMap} implementation for FLAC stream using binary search.
*
* <p>This seeker performs seeking by using binary search within the stream, until it finds the
* frame that contains the target sample.
*/
/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker {
/**
* Creates a {@link FlacBinarySearchSeeker}.
*
* @param flacStreamMetadata The stream metadata.
* @param frameStartMarker The frame start marker, consisting of the 2 bytes by which every frame
* in the stream must start.
* @param firstFramePosition The byte offset of the first frame in the stream.
* @param inputLength The length of the stream in bytes.
*/
public FlacBinarySearchSeeker(
FlacStreamMetadata flacStreamMetadata,
int frameStartMarker,
long firstFramePosition,
long inputLength) {
super(
/* seekTimestampConverter= */ flacStreamMetadata::getSampleNumber,
new FlacTimestampSeeker(flacStreamMetadata, frameStartMarker),
flacStreamMetadata.getDurationUs(),
/* floorTimePosition= */ 0,
/* ceilingTimePosition= */ flacStreamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ flacStreamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(
FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize));
}
private static final class FlacTimestampSeeker implements TimestampSeeker {
private final FlacStreamMetadata flacStreamMetadata;
private final int frameStartMarker;
private final SampleNumberHolder sampleNumberHolder;
private FlacTimestampSeeker(FlacStreamMetadata flacStreamMetadata, int frameStartMarker) {
this.flacStreamMetadata = flacStreamMetadata;
this.frameStartMarker = frameStartMarker;
sampleNumberHolder = new SampleNumberHolder();
}
@Override
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleNumber)
throws IOException, InterruptedException {
long searchPosition = input.getPosition();
// Find left frame.
long leftFrameFirstSampleNumber = findNextFrame(input);
long leftFramePosition = input.getPeekPosition();
input.advancePeekPosition(
Math.max(FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize));
// Find right frame.
long rightFrameFirstSampleNumber = findNextFrame(input);
long rightFramePosition = input.getPeekPosition();
if (leftFrameFirstSampleNumber <= targetSampleNumber
&& rightFrameFirstSampleNumber > targetSampleNumber) {
return TimestampSearchResult.targetFoundResult(leftFramePosition);
} else if (rightFrameFirstSampleNumber <= targetSampleNumber) {
return TimestampSearchResult.underestimatedResult(
rightFrameFirstSampleNumber, rightFramePosition);
} else {
return TimestampSearchResult.overestimatedResult(
leftFrameFirstSampleNumber, searchPosition);
}
}
/**
* Searches for the next frame in {@code input}.
*
* <p>The peek position is advanced to the start of the found frame, or at the end of the stream
* if no frame was found.
*
* @param input The input from which to search (starting from the peek position).
* @return The number of the first sample in the found frame, or the total number of samples in
* the stream if no frame was found.
* @throws IOException If peeking from the input fails. In this case, there is no guarantee on
* the peek position.
* @throws InterruptedException If interrupted while peeking from input. In this case, there is
* no guarantee on the peek position.
*/
private long findNextFrame(ExtractorInput input) throws IOException, InterruptedException {
while (input.getPeekPosition() < input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE
&& !FlacFrameReader.checkFrameHeaderFromPeek(
input, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) {
input.advancePeekPosition(1);
}
if (input.getPeekPosition() >= input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE) {
input.advancePeekPosition((int) (input.getLength() - input.getPeekPosition()));
return flacStreamMetadata.totalSamples;
}
return sampleNumberHolder.sampleNumber;
}
}
}
...@@ -69,9 +69,20 @@ import java.util.Collections; ...@@ -69,9 +69,20 @@ import java.util.Collections;
} else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
: MimeTypes.AUDIO_MLAW; : MimeTypes.AUDIO_MLAW;
int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; Format format =
Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, Format.createAudioSampleFormat(
Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); /* id= */ null,
/* sampleMimeType= */ type,
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
/* maxInputSize= */ Format.NO_VALUE,
/* channelCount= */ 1,
/* sampleRate= */ 8000,
/* pcmEncoding= */ Format.NO_VALUE,
/* initializationData= */ null,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
output.format(format); output.format(format);
hasOutputFormat = true; hasOutputFormat = true;
} else if (audioFormat != AUDIO_FORMAT_AAC) { } else if (audioFormat != AUDIO_FORMAT_AAC) {
......
...@@ -1248,10 +1248,10 @@ public class MatroskaExtractor implements Extractor { ...@@ -1248,10 +1248,10 @@ public class MatroskaExtractor implements Extractor {
if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) {
if (blockSampleCount > 1) { if (blockSampleCount > 1) {
Log.w(TAG, "Skipping subtitle sample in laced block."); Log.w(TAG, "Skipping subtitle sample in laced block.");
} else if (durationUs == C.TIME_UNSET) { } else if (blockDurationUs == C.TIME_UNSET) {
Log.w(TAG, "Skipping subtitle sample with no duration."); Log.w(TAG, "Skipping subtitle sample with no duration.");
} else { } else {
setSubtitleEndTime(track.codecId, durationUs, subtitleSample.data); setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data);
// Note: If we ever want to support DRM protected subtitles then we'll need to output the // Note: If we ever want to support DRM protected subtitles then we'll need to output the
// appropriate encryption data here. // appropriate encryption data here.
track.output.sampleData(subtitleSample, subtitleSample.limit()); track.output.sampleData(subtitleSample, subtitleSample.limit());
...@@ -1827,11 +1827,9 @@ public class MatroskaExtractor implements Extractor { ...@@ -1827,11 +1827,9 @@ public class MatroskaExtractor implements Extractor {
chunkSize += size; chunkSize += size;
chunkOffset = offset; // The offset is to the end of the sample. chunkOffset = offset; // The offset is to the end of the sample.
if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) {
// We haven't read enough samples to output a chunk.
return;
}
outputPendingSampleMetadata(track); outputPendingSampleMetadata(track);
} }
}
public void outputPendingSampleMetadata(Track track) { public void outputPendingSampleMetadata(Track track) {
if (chunkSampleCount > 0) { if (chunkSampleCount > 0) {
......
...@@ -379,6 +379,9 @@ import java.util.List; ...@@ -379,6 +379,9 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants") @SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_dfLa = 0x64664c61; public static final int TYPE_dfLa = 0x64664c61;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_twos = 0x74776f73;
public final int type; public final int type;
public Atom(int type) { public Atom(int type) {
......
...@@ -779,6 +779,7 @@ import java.util.List; ...@@ -779,6 +779,7 @@ import java.util.List;
|| childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_sawb
|| childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_lpcm
|| childAtomType == Atom.TYPE_sowt || childAtomType == Atom.TYPE_sowt
|| childAtomType == Atom.TYPE_twos
|| childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE__mp3
|| childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alac
|| childAtomType == Atom.TYPE_alaw || childAtomType == Atom.TYPE_alaw
...@@ -1039,6 +1040,7 @@ import java.util.List; ...@@ -1039,6 +1040,7 @@ import java.util.List;
int channelCount; int channelCount;
int sampleRate; int sampleRate;
@C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
channelCount = parent.readUnsignedShort(); channelCount = parent.readUnsignedShort();
...@@ -1099,6 +1101,10 @@ import java.util.List; ...@@ -1099,6 +1101,10 @@ import java.util.List;
mimeType = MimeTypes.AUDIO_AMR_WB; mimeType = MimeTypes.AUDIO_AMR_WB;
} else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) {
mimeType = MimeTypes.AUDIO_RAW; mimeType = MimeTypes.AUDIO_RAW;
pcmEncoding = C.ENCODING_PCM_16BIT;
} else if (atomType == Atom.TYPE_twos) {
mimeType = MimeTypes.AUDIO_RAW;
pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
} else if (atomType == Atom.TYPE__mp3) { } else if (atomType == Atom.TYPE__mp3) {
mimeType = MimeTypes.AUDIO_MPEG; mimeType = MimeTypes.AUDIO_MPEG;
} else if (atomType == Atom.TYPE_alac) { } else if (atomType == Atom.TYPE_alac) {
...@@ -1185,9 +1191,6 @@ import java.util.List; ...@@ -1185,9 +1191,6 @@ import java.util.List;
} }
if (out.format == null && mimeType != null) { if (out.format == null && mimeType != null) {
// TODO: Determine the correct PCM encoding.
@C.PcmEncoding int pcmEncoding =
MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE;
out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,
initializationData == null ? null : Collections.singletonList(initializationData), initializationData == null ? null : Collections.singletonList(initializationData),
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp4; package com.google.android.exoplayer2.extractor.mp4;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
...@@ -74,32 +75,205 @@ import java.nio.ByteBuffer; ...@@ -74,32 +75,205 @@ import java.nio.ByteBuffer;
private static final int PICTURE_TYPE_FRONT_COVER = 3; private static final int PICTURE_TYPE_FRONT_COVER = 3;
// Standard genres. // Standard genres.
private static final String[] STANDARD_GENRES = new String[] { @VisibleForTesting
/* package */ static final String[] STANDARD_GENRES =
new String[] {
// These are the official ID3v1 genres. // These are the official ID3v1 genres.
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Blues",
"Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", "Classic Rock",
"Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Country",
"Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", "Dance",
"Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul", "Disco",
"Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Funk",
"Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Grunge",
"Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Hip-Hop",
"Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Jazz",
"Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Metal",
"New Age",
"Oldies",
"Other",
"Pop",
"R&B",
"Rap",
"Reggae",
"Rock",
"Techno",
"Industrial",
"Alternative",
"Ska",
"Death Metal",
"Pranks",
"Soundtrack",
"Euro-Techno",
"Ambient",
"Trip-Hop",
"Vocal",
"Jazz+Funk",
"Fusion",
"Trance",
"Classical",
"Instrumental",
"Acid",
"House",
"Game",
"Sound Clip",
"Gospel",
"Noise",
"AlternRock",
"Bass",
"Soul",
"Punk",
"Space",
"Meditative",
"Instrumental Pop",
"Instrumental Rock",
"Ethnic",
"Gothic",
"Darkwave",
"Techno-Industrial",
"Electronic",
"Pop-Folk",
"Eurodance",
"Dream",
"Southern Rock",
"Comedy",
"Cult",
"Gangsta",
"Top 40",
"Christian Rap",
"Pop/Funk",
"Jungle",
"Native American",
"Cabaret",
"New Wave",
"Psychadelic",
"Rave",
"Showtunes",
"Trailer",
"Lo-Fi",
"Tribal",
"Acid Punk",
"Acid Jazz",
"Polka",
"Retro",
"Musical",
"Rock & Roll",
"Hard Rock", "Hard Rock",
// These were made up by the authors of Winamp and later added to the ID3 spec. // Genres made up by the authors of Winamp (v1.91) and later added to the ID3 spec.
"Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", "Folk",
"Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Folk-Rock",
"Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "National Folk",
"Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", "Swing",
"Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", "Fast Fusion",
"Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Bebob",
"Euro-House", "Dance Hall", "Latin",
// These were med up by the authors of Winamp but have not been added to the ID3 spec. "Revival",
"Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk", "Celtic",
"Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", "Bluegrass",
"Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "Avantgarde",
"Jpop", "Synthpop" "Gothic Rock",
"Progressive Rock",
"Psychedelic Rock",
"Symphonic Rock",
"Slow Rock",
"Big Band",
"Chorus",
"Easy Listening",
"Acoustic",
"Humour",
"Speech",
"Chanson",
"Opera",
"Chamber Music",
"Sonata",
"Symphony",
"Booty Bass",
"Primus",
"Porn Groove",
"Satire",
"Slow Jam",
"Club",
"Tango",
"Samba",
"Folklore",
"Ballad",
"Power Ballad",
"Rhythmic Soul",
"Freestyle",
"Duet",
"Punk Rock",
"Drum Solo",
"A capella",
"Euro-House",
"Dance Hall",
// Genres made up by the authors of Winamp (v1.91) but have not been added to the ID3 spec.
"Goa",
"Drum & Bass",
"Club-House",
"Hardcore",
"Terror",
"Indie",
"BritPop",
"Afro-Punk",
"Polsk Punk",
"Beat",
"Christian Gangsta Rap",
"Heavy Metal",
"Black Metal",
"Crossover",
"Contemporary Christian",
"Christian Rock",
"Merengue",
"Salsa",
"Thrash Metal",
"Anime",
"Jpop",
"Synthpop",
// Genres made up by the authors of Winamp (v5.6) but have not been added to the ID3 spec.
"Abstract",
"Art Rock",
"Baroque",
"Bhangra",
"Big beat",
"Breakbeat",
"Chillout",
"Downtempo",
"Dub",
"EBM",
"Eclectic",
"Electro",
"Electroclash",
"Emo",
"Experimental",
"Garage",
"Global",
"IDM",
"Illbient",
"Industro-Goth",
"Jam Band",
"Krautrock",
"Leftfield",
"Lounge",
"Math Rock",
"New Romantic",
"Nu-Breakz",
"Post-Punk",
"Post-Rock",
"Psytrance",
"Shoegaze",
"Space Rock",
"Trop Rock",
"World Music",
"Neoclassical",
"Audiobook",
"Audio theatre",
"Neue Deutsche Welle",
"Podcast",
"Indie-Rock",
"G-Funk",
"Dubstep",
"Garage Rock",
"Psybient"
}; };
private static final String LANGUAGE_UNDEFINED = "und"; private static final String LANGUAGE_UNDEFINED = "und";
......
...@@ -108,9 +108,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -108,9 +108,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private ParsableByteArray atomData; private ParsableByteArray atomData;
private int sampleTrackIndex; private int sampleTrackIndex;
private int sampleBytesRead;
private int sampleBytesWritten; private int sampleBytesWritten;
private int sampleCurrentNalBytesRemaining; private int sampleCurrentNalBytesRemaining;
private boolean isAc4HeaderRequired;
// Extractor outputs. // Extractor outputs.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
...@@ -158,9 +158,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -158,9 +158,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
containerAtoms.clear(); containerAtoms.clear();
atomHeaderBytesRead = 0; atomHeaderBytesRead = 0;
sampleTrackIndex = C.INDEX_UNSET; sampleTrackIndex = C.INDEX_UNSET;
sampleBytesRead = 0;
sampleBytesWritten = 0; sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0; sampleCurrentNalBytesRemaining = 0;
isAc4HeaderRequired = false;
if (position == 0) { if (position == 0) {
enterReadingAtomHeaderState(); enterReadingAtomHeaderState();
} else if (tracks != null) { } else if (tracks != null) {
...@@ -501,15 +501,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -501,15 +501,13 @@ public final class Mp4Extractor implements Extractor, SeekMap {
if (sampleTrackIndex == C.INDEX_UNSET) { if (sampleTrackIndex == C.INDEX_UNSET) {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
isAc4HeaderRequired =
MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType);
} }
Mp4Track track = tracks[sampleTrackIndex]; Mp4Track track = tracks[sampleTrackIndex];
TrackOutput trackOutput = track.trackOutput; TrackOutput trackOutput = track.trackOutput;
int sampleIndex = track.sampleIndex; int sampleIndex = track.sampleIndex;
long position = track.sampleTable.offsets[sampleIndex]; long position = track.sampleTable.offsets[sampleIndex];
int sampleSize = track.sampleTable.sizes[sampleIndex]; int sampleSize = track.sampleTable.sizes[sampleIndex];
long skipAmount = position - inputPosition + sampleBytesWritten; long skipAmount = position - inputPosition + sampleBytesRead;
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
positionHolder.position = position; positionHolder.position = position;
return RESULT_SEEK; return RESULT_SEEK;
...@@ -537,6 +535,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -537,6 +535,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
if (sampleCurrentNalBytesRemaining == 0) { if (sampleCurrentNalBytesRemaining == 0) {
// Read the NAL length so that we know where we find the next one. // Read the NAL length so that we know where we find the next one.
input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
sampleBytesRead += nalUnitLengthFieldLength;
nalLength.setPosition(0); nalLength.setPosition(0);
int nalLengthInt = nalLength.readInt(); int nalLengthInt = nalLength.readInt();
if (nalLengthInt < 0) { if (nalLengthInt < 0) {
...@@ -551,21 +550,23 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -551,21 +550,23 @@ public final class Mp4Extractor implements Extractor, SeekMap {
} else { } else {
// Write the payload of the NAL unit. // Write the payload of the NAL unit.
int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false);
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes; sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes;
} }
} }
} else { } else {
if (isAc4HeaderRequired) { if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) {
if (sampleBytesWritten == 0) {
Ac4Util.getAc4SampleHeader(sampleSize, scratch); Ac4Util.getAc4SampleHeader(sampleSize, scratch);
int length = scratch.limit(); trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
trackOutput.sampleData(scratch, length); sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE;
sampleSize += length; }
sampleBytesWritten += length; sampleSize += Ac4Util.SAMPLE_HEADER_SIZE;
isAc4HeaderRequired = false;
} }
while (sampleBytesWritten < sampleSize) { while (sampleBytesWritten < sampleSize) {
int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false);
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes; sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes;
} }
...@@ -574,6 +575,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -574,6 +575,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
track.sampleTable.flags[sampleIndex], sampleSize, 0, null); track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
track.sampleIndex++; track.sampleIndex++;
sampleTrackIndex = C.INDEX_UNSET; sampleTrackIndex = C.INDEX_UNSET;
sampleBytesRead = 0;
sampleBytesWritten = 0; sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0; sampleCurrentNalBytesRemaining = 0;
return RESULT_CONTINUE; return RESULT_CONTINUE;
......
...@@ -15,18 +15,18 @@ ...@@ -15,18 +15,18 @@
*/ */
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.FlacFrameReader;
import com.google.android.exoplayer2.extractor.FlacMetadataReader;
import com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacConstants;
import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** /**
* {@link StreamReader} to extract Flac data out of Ogg byte stream. * {@link StreamReader} to extract Flac data out of Ogg byte stream.
...@@ -34,7 +34,6 @@ import java.util.List; ...@@ -34,7 +34,6 @@ import java.util.List;
/* package */ final class FlacReader extends StreamReader { /* package */ final class FlacReader extends StreamReader {
private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF; private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;
private static final byte SEEKTABLE_PACKET_TYPE = 0x03;
private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
...@@ -72,27 +71,13 @@ import java.util.List; ...@@ -72,27 +71,13 @@ import java.util.List;
byte[] data = packet.data; byte[] data = packet.data;
if (streamMetadata == null) { if (streamMetadata == null) {
streamMetadata = new FlacStreamMetadata(data, 17); streamMetadata = new FlacStreamMetadata(data, 17);
int maxInputSize =
streamMetadata.maxFrameSize == 0 ? Format.NO_VALUE : streamMetadata.maxFrameSize;
byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null);
List<byte[]> initializationData = Collections.singletonList(metadata); } else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) {
setupData.format =
Format.createAudioSampleFormat(
/* id= */ null,
MimeTypes.AUDIO_FLAC,
/* codecs= */ null,
streamMetadata.bitRate(),
maxInputSize,
streamMetadata.channels,
streamMetadata.sampleRate,
initializationData,
/* drmInitData= */ null,
/* selectionFlags= */ 0,
/* language= */ null);
} else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {
flacOggSeeker = new FlacOggSeeker(); flacOggSeeker = new FlacOggSeeker();
flacOggSeeker.parseSeekTable(packet); FlacStreamMetadata.SeekTable seekTable =
FlacMetadataReader.readSeekTableMetadataBlock(packet);
streamMetadata = streamMetadata.copyWithSeekTable(seekTable);
} else if (isAudioPacket(data)) { } else if (isAudioPacket(data)) {
if (flacOggSeeker != null) { if (flacOggSeeker != null) {
flacOggSeeker.setFirstFrameOffset(position); flacOggSeeker.setFirstFrameOffset(position);
...@@ -104,44 +89,19 @@ import java.util.List; ...@@ -104,44 +89,19 @@ import java.util.List;
} }
private int getFlacFrameBlockSize(ParsableByteArray packet) { private int getFlacFrameBlockSize(ParsableByteArray packet) {
int blockSizeCode = (packet.data[2] & 0xFF) >> 4; int blockSizeKey = (packet.data[2] & 0xFF) >> 4;
switch (blockSizeCode) { if (blockSizeKey == 6 || blockSizeKey == 7) {
case 1: // Skip the sample number.
return 192;
case 2:
case 3:
case 4:
case 5:
return 576 << (blockSizeCode - 2);
case 6:
case 7:
// skip the sample number
packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
packet.readUtf8EncodedLong(); packet.readUtf8EncodedLong();
int value = blockSizeCode == 6 ? packet.readUnsignedByte() : packet.readUnsignedShort();
packet.setPosition(0);
return value + 1;
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
return 256 << (blockSizeCode - 8);
default:
return -1;
} }
int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(packet, blockSizeKey);
packet.setPosition(0);
return result;
} }
private class FlacOggSeeker implements OggSeeker, SeekMap { private class FlacOggSeeker implements OggSeeker {
private static final int METADATA_LENGTH_OFFSET = 1;
private static final int SEEK_POINT_SIZE = 18;
private long[] seekPointGranules;
private long[] seekPointOffsets;
private long firstFrameOffset; private long firstFrameOffset;
private long pendingSeekGranule; private long pendingSeekGranule;
...@@ -154,27 +114,6 @@ import java.util.List; ...@@ -154,27 +114,6 @@ import java.util.List;
this.firstFrameOffset = firstFrameOffset; this.firstFrameOffset = firstFrameOffset;
} }
/**
* Parses a FLAC file seek table metadata structure and initializes internal fields.
*
* @param data A {@link ParsableByteArray} including whole seek table metadata block. Its
* position should be set to the beginning of the block.
* @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
* METADATA_BLOCK_SEEKTABLE</a>
*/
public void parseSeekTable(ParsableByteArray data) {
data.skipBytes(METADATA_LENGTH_OFFSET);
int length = data.readUnsignedInt24();
int numberOfSeekPoints = length / SEEK_POINT_SIZE;
seekPointGranules = new long[numberOfSeekPoints];
seekPointOffsets = new long[numberOfSeekPoints];
for (int i = 0; i < numberOfSeekPoints; i++) {
seekPointGranules[i] = data.readLong();
seekPointOffsets[i] = data.readLong();
data.skipBytes(2); // Skip "Number of samples in the target frame."
}
}
@Override @Override
public long read(ExtractorInput input) throws IOException, InterruptedException { public long read(ExtractorInput input) throws IOException, InterruptedException {
if (pendingSeekGranule >= 0) { if (pendingSeekGranule >= 0) {
...@@ -187,40 +126,16 @@ import java.util.List; ...@@ -187,40 +126,16 @@ import java.util.List;
@Override @Override
public void startSeek(long targetGranule) { public void startSeek(long targetGranule) {
Assertions.checkNotNull(streamMetadata.seekTable);
long[] seekPointGranules = streamMetadata.seekTable.pointSampleNumbers;
int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true);
pendingSeekGranule = seekPointGranules[index]; pendingSeekGranule = seekPointGranules[index];
} }
@Override @Override
public SeekMap createSeekMap() { public SeekMap createSeekMap() {
return this; Assertions.checkState(firstFrameOffset != -1);
} return new FlacSeekTableSeekMap(streamMetadata, firstFrameOffset);
@Override
public boolean isSeekable() {
return true;
}
@Override
public SeekPoints getSeekPoints(long timeUs) {
long granule = convertTimeToGranule(timeUs);
int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
long seekTimeUs = convertGranuleToTime(seekPointGranules[index]);
long seekPosition = firstFrameOffset + seekPointOffsets[index];
SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
if (seekTimeUs >= timeUs || index == seekPointGranules.length - 1) {
return new SeekPoints(seekPoint);
} else {
long secondSeekTimeUs = convertGranuleToTime(seekPointGranules[index + 1]);
long secondSeekPosition = firstFrameOffset + seekPointOffsets[index + 1];
SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
return new SeekPoints(seekPoint, secondSeekPoint);
}
}
@Override
public long getDurationUs() {
return streamMetadata.durationUs();
} }
} }
......
No preview for this file type
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment