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 2569 additions and 749 deletions
......@@ -58,6 +58,7 @@ extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv
# AV1 extension
extensions/av1/src/main/jni/cpu_features
extensions/av1/src/main/jni/libgav1
# Opus extension
......
# 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) ###
* UI: Exclude `DefaultTimeBar` region from system gesture detection
......@@ -165,7 +225,7 @@
`C.MSG_SET_OUTPUT_BUFFER_RENDERER`.
* Use `VideoDecoderRenderer` as an implementation of
`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.
* FFmpeg extension:
* Update to use NDK r20.
......@@ -302,7 +362,7 @@
([#6241](https://github.com/google/ExoPlayer/issues/6241)).
* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change
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)).
* Fix issue where initial seek positions get ignored when playing a preroll ad
([#6201](https://github.com/google/ExoPlayer/issues/6201)).
......@@ -311,7 +371,7 @@
([#6153](https://github.com/google/ExoPlayer/issues/6153)).
* Fix `DataSchemeDataSource` re-opening and range requests
([#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)).
* Fix issue when calling `performClick` on `PlayerView` without
`PlayerControlView`
......
......@@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.11.1'
releaseVersionCode = 2011001
releaseVersion = '2.11.2'
releaseVersionCode = 2011002
minSdkVersion = 16
appTargetSdkVersion = 29
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 @@
"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)",
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
},
......@@ -596,6 +600,13 @@
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
"subtitle_mime_type": "application/ttml+xml",
"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 @@
package com.google.android.exoplayer2.demo;
import android.app.Notification;
import android.content.Context;
import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService;
......@@ -32,8 +33,6 @@ public class DemoDownloadService extends DownloadService {
private static final int JOB_ID = 1;
private static final int FOREGROUND_NOTIFICATION_ID = 1;
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
private DownloadNotificationHelper notificationHelper;
public DemoDownloadService() {
......@@ -43,7 +42,6 @@ public class DemoDownloadService extends DownloadService {
CHANNEL_ID,
R.string.exo_download_notification_channel_name,
/* channelDescriptionResourceId= */ 0);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
}
@Override
......@@ -54,7 +52,13 @@ public class DemoDownloadService extends DownloadService {
@Override
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
......@@ -68,24 +72,45 @@ public class DemoDownloadService extends DownloadService {
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
}
@Override
protected void onDownloadChanged(Download download) {
Notification notification;
if (download.state == Download.STATE_COMPLETED) {
notification =
notificationHelper.buildDownloadCompletedNotification(
R.drawable.ic_download_done,
/* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data));
} else if (download.state == Download.STATE_FAILED) {
notification =
notificationHelper.buildDownloadFailedNotification(
R.drawable.ic_download_done,
/* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data));
} else {
return;
/**
* 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
public void onDownloadChanged(DownloadManager manager, Download download) {
Notification notification;
if (download.state == Download.STATE_COMPLETED) {
notification =
notificationHelper.buildDownloadCompletedNotification(
R.drawable.ic_download_done,
/* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data));
} else if (download.state == Download.STATE_FAILED) {
notification =
notificationHelper.buildDownloadFailedNotification(
R.drawable.ic_download_done,
/* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data));
} else {
return;
}
NotificationUtil.setNotification(context, nextNotificationId++, notification);
}
NotificationUtil.setNotification(this, nextNotificationId++, notification);
}
}
......@@ -432,6 +432,11 @@ public class PlayerActivity extends AppCompatActivity
mediaSources[i] = createLeafMediaSource(samples[i]);
Sample.SubtitleInfo subtitleInfo = samples[i].subtitleInfo;
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.createTextSampleFormat(
/* id= */ null,
......@@ -507,7 +512,7 @@ public class PlayerActivity extends AppCompatActivity
}
private MediaSource createLeafMediaSource(
Uri uri, String extension, DrmSessionManager<ExoMediaCrypto> drmSessionManager) {
Uri uri, String extension, DrmSessionManager<?> drmSessionManager) {
@ContentType int type = Util.inferContentType(uri, extension);
switch (type) {
case C.TYPE_DASH:
......@@ -615,10 +620,20 @@ public class PlayerActivity extends AppCompatActivity
}
MediaSourceFactory adMediaSourceFactory =
new MediaSourceFactory() {
private DrmSessionManager<?> drmSessionManager =
DrmSessionManager.getDummyDrmSessionManager();
@Override
public MediaSourceFactory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
this.drmSessionManager = drmSessionManager;
return this;
}
@Override
public MediaSource createMediaSource(Uri uri) {
return PlayerActivity.this.createLeafMediaSource(
uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager());
uri, /* extension=*/ null, drmSessionManager);
}
@Override
......
......@@ -11,9 +11,15 @@ project(libgav1JNI C CXX)
# armeabi-v7a build. This flag enables it.
if(${ANDROID_ABI} MATCHES "armeabi-v7a")
add_compile_options("-mfpu=neon")
add_compile_options("-marm")
add_compile_options("-fPIC")
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_build "${CMAKE_BINARY_DIR}")
set(libgav1_jni_output_directory
......
......@@ -98,8 +98,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding)
|| !isOutputSupported(format)) {
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType) || !isOutputSupported(format)) {
return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
......
......@@ -64,9 +64,7 @@ import java.util.List;
throw new FfmpegDecoderException("Failed to load decoder native libraries.");
}
Assertions.checkNotNull(format.sampleMimeType);
codecName =
Assertions.checkNotNull(
FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding));
codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType));
extraData = getExtraData(format.sampleMimeType, format.initializationData);
encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT;
......@@ -145,16 +143,12 @@ import java.util.List;
nativeContext = 0;
}
/**
* Returns the channel count of output audio. May only be called after {@link #decode}.
*/
/** Returns the channel count of output audio. */
public int getChannelCount() {
return channelCount;
}
/**
* Returns the sample rate of output audio. May only be called after {@link #decode}.
*/
/** Returns the sample rate of output audio. */
public int getSampleRate() {
return sampleRate;
}
......
......@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.ext.ffmpeg;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.Log;
......@@ -65,13 +64,12 @@ public final class FfmpegLibrary {
* Returns whether the underlying library supports the specified MIME type.
*
* @param mimeType The MIME type to check.
* @param encoding The PCM encoding for raw audio.
*/
public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) {
public static boolean supportsFormat(String mimeType) {
if (!isAvailable()) {
return false;
}
String codecName = getCodecName(mimeType, encoding);
String codecName = getCodecName(mimeType);
if (codecName == null) {
return false;
}
......@@ -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}
* if it's unsupported.
*/
/* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) {
/* package */ static @Nullable String getCodecName(String mimeType) {
switch (mimeType) {
case MimeTypes.AUDIO_AAC:
return "aac";
......@@ -116,14 +114,10 @@ public final class FfmpegLibrary {
return "flac";
case MimeTypes.AUDIO_ALAC:
return "alac";
case MimeTypes.AUDIO_RAW:
if (encoding == C.ENCODING_PCM_MU_LAW) {
return "pcm_mulaw";
} else if (encoding == C.ENCODING_PCM_A_LAW) {
return "pcm_alaw";
} else {
return null;
}
case MimeTypes.AUDIO_MLAW:
return "pcm_mulaw";
case MimeTypes.AUDIO_ALAW:
return "pcm_alaw";
default:
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:
selectionFlags = 0
language = null
drmInitData = -
metadata = null
initializationData:
total output bytes = 526272
sample count = 33
......
......@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
metadata = null
initializationData:
total output bytes = 362432
sample count = 23
......
......@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
metadata = null
initializationData:
total output bytes = 182208
sample count = 12
......
......@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
drmInitData = -
metadata = null
initializationData:
total output bytes = 18368
sample count = 2
......
......@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
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:
total output bytes = 526272
sample count = 33
......
......@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
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:
total output bytes = 362432
sample count = 23
......
......@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
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:
total output bytes = 182208
sample count = 12
......
......@@ -24,6 +24,7 @@ track 0:
selectionFlags = 0
language = null
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:
total output bytes = 18368
sample count = 2
......
......@@ -20,10 +20,12 @@ import static org.junit.Assert.fail;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ext.flac.FlacBinarySearchSeeker.OutputFrameHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -47,19 +49,20 @@ public final class FlacBinarySearchSeekerTest {
throws IOException, FlacDecoderException, InterruptedException {
byte[] data =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input);
OutputFrameHolder outputFrameHolder = new OutputFrameHolder(ByteBuffer.allocateDirect(0));
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
decoderJni,
outputFrameHolder);
SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull();
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
assertThat(seekMap.isSeekable()).isTrue();
......@@ -70,18 +73,20 @@ public final class FlacBinarySearchSeekerTest {
throws IOException, FlacDecoderException, InterruptedException {
byte[] data =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
FlacDecoderJni decoderJni = new FlacDecoderJni();
decoderJni.setData(input);
OutputFrameHolder outputFrameHolder = new OutputFrameHolder(ByteBuffer.allocateDirect(0));
FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker(
decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
decoderJni,
outputFrameHolder);
seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue();
}
}
......@@ -25,9 +25,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
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.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.testutil.CapturingAudioSink;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
import org.junit.Test;
......@@ -37,7 +41,8 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
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
public void setUp() {
......@@ -47,38 +52,56 @@ public class FlacPlaybackTest {
}
@Test
public void testBasicPlayback() throws Exception {
playUri(BEAR_FLAC_URI);
public void test16BitPlayback() throws Exception {
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 =
new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
new TestPlaybackRunnable(
Uri.parse("asset:///" + fileName),
ApplicationProvider.getApplicationContext(),
audioSink);
Thread thread = new Thread(testPlaybackRunnable);
thread.start();
thread.join();
if (testPlaybackRunnable.playbackException != null) {
throw testPlaybackRunnable.playbackException;
}
audioSink.assertOutput(
ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump");
}
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
private final Context context;
private final Uri uri;
private final AudioSink audioSink;
private ExoPlayer player;
private ExoPlaybackException playbackException;
public TestPlaybackRunnable(Uri uri, Context context) {
public TestPlaybackRunnable(Uri uri, Context context, AudioSink audioSink) {
this.uri = uri;
this.context = context;
this.audioSink = audioSink;
}
@Override
public void run() {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
LibflacAudioRenderer audioRenderer =
new LibflacAudioRenderer(/* eventHandler= */ null, /* eventListener= */ null, audioSink);
player = new ExoPlayer.Builder(context, audioRenderer).build();
player.addListener(this);
MediaSource mediaSource =
......@@ -105,5 +128,4 @@ public class FlacPlaybackTest {
}
}
}
}
......@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacConstants;
import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException;
import java.nio.ByteBuffer;
......@@ -31,23 +32,50 @@ import java.nio.ByteBuffer;
*/
/* 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;
/**
* 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(
FlacStreamMetadata streamMetadata,
long firstFramePosition,
long inputLength,
FlacDecoderJni decoderJni) {
FlacDecoderJni decoderJni,
OutputFrameHolder outputFrameHolder) {
super(
new FlacSeekTimestampConverter(streamMetadata),
new FlacTimestampSeeker(decoderJni),
streamMetadata.durationUs(),
/* seekTimestampConverter= */ streamMetadata::getSampleNumber,
new FlacTimestampSeeker(decoderJni, outputFrameHolder),
streamMetadata.getDurationUs(),
/* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
/* minimumSearchRange= */ Math.max(
FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni);
}
......@@ -63,14 +91,15 @@ import java.nio.ByteBuffer;
private static final class FlacTimestampSeeker implements TimestampSeeker {
private final FlacDecoderJni decoderJni;
private final OutputFrameHolder outputFrameHolder;
private FlacTimestampSeeker(FlacDecoderJni decoderJni) {
private FlacTimestampSeeker(FlacDecoderJni decoderJni, OutputFrameHolder outputFrameHolder) {
this.decoderJni = decoderJni;
this.outputFrameHolder = outputFrameHolder;
}
@Override
public TimestampSearchResult searchForTimestamp(
ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder)
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleIndex)
throws IOException, InterruptedException {
ByteBuffer outputBuffer = outputFrameHolder.byteBuffer;
long searchPosition = input.getPosition();
......@@ -97,6 +126,8 @@ import java.nio.ByteBuffer;
if (targetSampleInLastFrame) {
// We are holding the target frame in outputFrameHolder. Set its presentation time now.
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());
} else if (nextFrameSampleIndex <= targetSampleIndex) {
return TimestampSearchResult.underestimatedResult(
......@@ -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;
/* package */ final class FlacDecoder extends
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FlacDecoderException> {
private final int maxOutputBufferSize;
private final FlacStreamMetadata streamMetadata;
private final FlacDecoderJni decoderJni;
/**
......@@ -59,7 +59,6 @@ import java.util.List;
}
decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
FlacStreamMetadata streamMetadata;
try {
streamMetadata = decoderJni.decodeStreamMetadata();
} catch (ParserException e) {
......@@ -72,7 +71,6 @@ import java.util.List;
int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
}
@Override
......@@ -103,7 +101,8 @@ import java.util.List;
decoderJni.flush();
}
decoderJni.setData(Util.castNonNull(inputBuffer.data));
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize);
ByteBuffer outputData =
outputBuffer.init(inputBuffer.timeUs, streamMetadata.getMaxDecodedFrameSize());
try {
decoderJni.decodeSample(outputData);
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
......@@ -121,4 +120,8 @@ import java.util.List;
decoderJni.release();
}
/** Returns the {@link FlacStreamMetadata} decoded from the initialization data. */
public FlacStreamMetadata getStreamMetadata() {
return streamMetadata;
}
}
......@@ -21,18 +21,17 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
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.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
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.SeekMap;
import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
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.FlacStreamMetadata;
import com.google.android.exoplayer2.util.MimeTypes;
......@@ -42,7 +41,6 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
......@@ -72,11 +70,7 @@ public final class FlacExtractor implements Extractor {
*/
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 Id3Peeker id3Peeker;
private final boolean id3MetadataDisabled;
@Nullable private FlacDecoderJni decoderJni;
......@@ -90,7 +84,7 @@ public final class FlacExtractor implements Extractor {
@Nullable private Metadata id3Metadata;
@Nullable private FlacBinarySearchSeeker binarySearchSeeker;
/** Constructs an instance with flags = 0. */
/** Constructs an instance with {@code flags = 0}. */
public FlacExtractor() {
this(/* flags= */ 0);
}
......@@ -98,11 +92,11 @@ public final class FlacExtractor implements Extractor {
/**
* 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) {
outputBuffer = new ParsableByteArray();
id3Peeker = new Id3Peeker();
id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
}
......@@ -120,17 +114,15 @@ public final class FlacExtractor implements Extractor {
@Override
public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
if (input.getPosition() == 0) {
id3Metadata = peekId3Data(input);
}
return peekFlacStreamMarker(input);
id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
return FlacMetadataReader.checkAndPeekStreamMarker(input);
}
@Override
public int read(final ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
id3Metadata = peekId3Data(input);
id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true);
}
FlacDecoderJni decoderJni = initDecoderJni(input);
......@@ -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.
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
private FlacDecoderJni initDecoderJni(ExtractorInput input) {
......@@ -211,11 +190,12 @@ public final class FlacExtractor implements Extractor {
return;
}
FlacDecoderJni flacDecoderJni = decoderJni;
FlacStreamMetadata streamMetadata;
try {
streamMetadata = decoderJni.decodeStreamMetadata();
streamMetadata = flacDecoderJni.decodeStreamMetadata();
} catch (IOException e) {
decoderJni.reset(/* newPosition= */ 0);
flacDecoderJni.reset(/* newPosition= */ 0);
input.setRetryPosition(/* position= */ 0, e);
throw e;
}
......@@ -223,15 +203,18 @@ public final class FlacExtractor implements Extractor {
streamMetadataDecoded = true;
if (this.streamMetadata == null) {
this.streamMetadata = streamMetadata;
outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
binarySearchSeeker =
outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
if (streamMetadata.metadata != null) {
metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
}
outputSeekMap(
flacDecoderJni,
streamMetadata,
input.getLength(),
extractorOutput,
outputFrameHolder);
@Nullable
Metadata metadata = streamMetadata.getMetadataCopyWithAppendedEntriesFrom(id3Metadata);
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 {
OutputFrameHolder outputFrameHolder,
TrackOutput trackOutput)
throws InterruptedException, IOException {
int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition);
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
......@@ -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
* handle seeks.
*/
......@@ -272,19 +243,21 @@ public final class FlacExtractor implements Extractor {
FlacDecoderJni decoderJni,
FlacStreamMetadata streamMetadata,
long streamLength,
ExtractorOutput output) {
ExtractorOutput output,
OutputFrameHolder outputFrameHolder) {
boolean haveSeekTable = decoderJni.getSeekPoints(/* timeUs= */ 0) != null;
FlacBinarySearchSeeker binarySearchSeeker = null;
SeekMap seekMap;
if (haveSeekTable) {
seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni);
} else if (streamLength != C.LENGTH_UNSET) {
long firstFramePosition = decoderJni.getDecodePosition();
binarySearchSeeker =
new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
new FlacBinarySearchSeeker(
streamMetadata, firstFramePosition, streamLength, decoderJni, outputFrameHolder);
seekMap = binarySearchSeeker.getSeekMap();
} else {
seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
seekMap = new SeekMap.Unseekable(streamMetadata.getDurationUs());
}
output.seekMap(seekMap);
return binarySearchSeeker;
......@@ -297,8 +270,8 @@ public final class FlacExtractor implements Extractor {
/* id= */ null,
MimeTypes.AUDIO_RAW,
/* codecs= */ null,
streamMetadata.bitRate(),
streamMetadata.maxDecodedFrameSize(),
streamMetadata.getBitRate(),
streamMetadata.getMaxDecodedFrameSize(),
streamMetadata.channels,
streamMetadata.sampleRate,
getPcmEncoding(streamMetadata.bitsPerSample),
......
......@@ -21,18 +21,24 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
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.drm.DrmSessionManager;
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.Util;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Decodes and renders audio using the native Flac decoder.
*/
public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
/** Decodes and renders audio using the native Flac decoder. */
public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
private static final int NUM_BUFFERS = 16;
@MonotonicNonNull private FlacStreamMetadata streamMetadata;
public LibflacAudioRenderer() {
this(/* eventHandler= */ null, /* eventListener= */ null);
}
......@@ -50,6 +56,24 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
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
@FormatSupport
protected int supportsFormatInternal(
......@@ -57,7 +81,23 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
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;
} else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return FORMAT_UNSUPPORTED_DRM;
......@@ -69,8 +109,27 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
@Override
protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FlacDecoderException {
return new FlacDecoder(
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
FlacDecoder decoder =
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) {
"FlacStreamMetadata");
jmethodID flacStreamMetadataConstructor =
env->GetMethodID(flacStreamMetadataClass, "<init>",
"(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
"(IIIIIIIJLjava/util/ArrayList;Ljava/util/ArrayList;)V");
return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
streamInfo.min_blocksize, streamInfo.max_blocksize,
......
......@@ -349,26 +349,6 @@ bool FLACParser::decodeMetadata() {
ALOGE("unsupported bits per sample %u", getBitsPerSample());
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.
if (isBigEndian()) {
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 {
* {@link #schedule(Requirements, String, String)} or {@link #cancel()} are called.
*/
public JobDispatcherScheduler(Context context, String jobTag) {
this.jobDispatcher =
new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext()));
context = context.getApplicationContext();
this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context));
this.jobTag = jobTag;
}
......
......@@ -178,8 +178,8 @@ public final class MediaSessionConnector {
Player player,
ControlDispatcher controlDispatcher,
String command,
Bundle extras,
ResultReceiver cb);
@Nullable Bundle extras,
@Nullable ResultReceiver cb);
}
/** Interface to which playback preparation and play actions are delegated. */
......@@ -394,6 +394,7 @@ public final class MediaSessionConnector {
* @param player The player connected to the media session.
* @return The custom action to be included in the session playback state or {@code null}.
*/
@Nullable
PlaybackStateCompat.CustomAction getCustomAction(Player player);
}
......@@ -1293,7 +1294,7 @@ public final class MediaSessionConnector {
}
@Override
public void onCommand(String command, Bundle extras, ResultReceiver cb) {
public void onCommand(String command, @Nullable Bundle extras, @Nullable ResultReceiver cb) {
if (player != null) {
for (int i = 0; i < commandReceivers.size(); i++) {
if (commandReceivers.get(i).onCommand(player, controlDispatcher, command, extras, cb)) {
......
......@@ -191,9 +191,9 @@ public final class TimelineQueueEditor
Player player,
ControlDispatcher controlDispatcher,
String command,
Bundle extras,
ResultReceiver cb) {
if (!COMMAND_MOVE_QUEUE_ITEM.equals(command)) {
@Nullable Bundle extras,
@Nullable ResultReceiver cb) {
if (!COMMAND_MOVE_QUEUE_ITEM.equals(command) || extras == null) {
return false;
}
int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET);
......
......@@ -186,8 +186,8 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu
Player player,
ControlDispatcher controlDispatcher,
String command,
Bundle extras,
ResultReceiver cb) {
@Nullable Bundle extras,
@Nullable ResultReceiver cb) {
return false;
}
......
......@@ -39,9 +39,9 @@ dependencies {
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
// 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
// Since OkHttp is distributed as a jar rather than an aar, Gradle wont stop
// us from making this mistake!
api 'com.squareup.okhttp3:okhttp:3.12.5'
// Since OkHttp is distributed as a jar rather than an aar, Gradle won't
// stop us from making this mistake!
api 'com.squareup.okhttp3:okhttp:3.12.8'
}
ext {
......
......@@ -5,6 +5,12 @@
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.
-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer {
*;
......
......@@ -95,14 +95,16 @@ public final class C {
* The name of the ASCII charset.
*/
public static final String ASCII_NAME = "US-ASCII";
/**
* The name of the UTF-8 charset.
*/
public static final String UTF8_NAME = "UTF-8";
/**
* The name of the UTF-16 charset.
*/
/** The name of the ISO-8859-1 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";
/** The name of the UTF-16 little-endian charset. */
......@@ -148,8 +150,8 @@ public final class C {
/**
* 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
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
* #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link
* #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
* {@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},
* {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
*/
......@@ -160,26 +162,26 @@ public final class C {
ENCODING_INVALID,
ENCODING_PCM_8BIT,
ENCODING_PCM_16BIT,
ENCODING_PCM_16BIT_BIG_ENDIAN,
ENCODING_PCM_24BIT,
ENCODING_PCM_32BIT,
ENCODING_PCM_FLOAT,
ENCODING_PCM_MU_LAW,
ENCODING_PCM_A_LAW,
ENCODING_MP3,
ENCODING_AC3,
ENCODING_E_AC3,
ENCODING_E_AC3_JOC,
ENCODING_AC4,
ENCODING_DTS,
ENCODING_DTS_HD,
ENCODING_DOLBY_TRUEHD,
ENCODING_DOLBY_TRUEHD
})
public @interface Encoding {}
/**
* 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
* #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link
* #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}.
* #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
* {@link #ENCODING_PCM_FLOAT}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
......@@ -188,11 +190,10 @@ public final class C {
ENCODING_INVALID,
ENCODING_PCM_8BIT,
ENCODING_PCM_16BIT,
ENCODING_PCM_16BIT_BIG_ENDIAN,
ENCODING_PCM_24BIT,
ENCODING_PCM_32BIT,
ENCODING_PCM_FLOAT,
ENCODING_PCM_MU_LAW,
ENCODING_PCM_A_LAW
ENCODING_PCM_FLOAT
})
public @interface PcmEncoding {}
/** @see AudioFormat#ENCODING_INVALID */
......@@ -201,16 +202,16 @@ public final class C {
public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
/** @see 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. */
public static final int ENCODING_PCM_24BIT = 0x80000000;
public static final int ENCODING_PCM_24BIT = 0x20000000;
/** 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 */
public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
/** Audio encoding for mu-law. */
public static final int ENCODING_PCM_MU_LAW = 0x10000000;
/** Audio encoding for A-law. */
public static final int ENCODING_PCM_A_LAW = 0x20000000;
/** @see AudioFormat#ENCODING_MP3 */
public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3;
/** @see AudioFormat#ENCODING_AC3 */
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/** @see AudioFormat#ENCODING_E_AC3 */
......@@ -976,8 +977,8 @@ public final class C {
/**
* 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
* #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or
* {@link #NETWORK_TYPE_OTHER}.
* #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link
* #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
......@@ -988,6 +989,7 @@ public final class C {
NETWORK_TYPE_2G,
NETWORK_TYPE_3G,
NETWORK_TYPE_4G,
NETWORK_TYPE_5G,
NETWORK_TYPE_CELLULAR_UNKNOWN,
NETWORK_TYPE_ETHERNET,
NETWORK_TYPE_OTHER
......@@ -1005,6 +1007,8 @@ public final class C {
public static final int NETWORK_TYPE_3G = 4;
/** Network type for a 4G cellular connection. */
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_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}.
......@@ -1012,10 +1016,7 @@ public final class C {
public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6;
/** Network type for an Ethernet connection. */
public static final int NETWORK_TYPE_ETHERNET = 7;
/**
* Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN,
* Bluetooth).
*/
/** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */
public static final int NETWORK_TYPE_OTHER = 8;
/**
......
......@@ -93,8 +93,8 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
*
* <p>The figure below shows ExoPlayer's threading model.
*
* <p align="center"><img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's threading
* model">
* <p style="align:center"><img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's
* threading model">
*
* <ul>
* <li>ExoPlayer instances must be accessed from a single application thread. For the vast
......
......@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** 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.
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}. */
// 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.
......@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// 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}
......
......@@ -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.
*/
public final int sampleRate;
/**
* 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.
*/
/** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */
public final @C.PcmEncoding int pcmEncoding;
/**
* 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;
* valid state transitions are shown below, annotated with the methods that are called during each
* 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 {
......
......@@ -44,7 +44,7 @@ import com.google.android.exoplayer2.util.Assertions;
*
* <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
* 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
......@@ -52,17 +52,17 @@ import com.google.android.exoplayer2.util.Assertions;
*
* <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
* of files"> A timeline for a playlist of media files or on-demand streams consists of multiple
* periods, each with its own window. Each window spans the whole of the corresponding period, and
* typically has a default position at the start of the period. The properties of the periods and
* windows (e.g. their durations and whether the window is seekable) will often only become known
* when the player starts buffering the corresponding file or stream.
* <p style="align:center"><img src="doc-files/timeline-playlist.svg" alt="Example timeline for a
* playlist of files"> A timeline for a playlist of media files or on-demand streams consists of
* multiple periods, each with its own window. Each window spans the whole of the corresponding
* period, and typically has a default position at the start of the period. The properties of the
* periods and windows (e.g. their durations and whether the window is seekable) will often only
* become known when the player starts buffering the corresponding file or stream.
*
* <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
* stream with limited availability"> A timeline for a live stream consists of a period whose
* <p style="align:center"><img src="doc-files/timeline-live-limited.svg" alt="Example timeline for
* 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
* 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
......@@ -72,24 +72,24 @@ import com.google.android.exoplayer2.util.Assertions;
*
* <h3>Live stream with indefinite availability</h3>
*
* <p align="center"><img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline for a
* live stream with indefinite availability"> A timeline for a live stream with indefinite
* <p style="align:center"><img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline
* 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>
* 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.
*
* <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
* live stream with multiple periods"> This case arises when a live stream is explicitly divided
* into separate periods, for example at content boundaries. This case is similar to the <a
* <p style="align:center"><img src="doc-files/timeline-live-multi-period.svg" alt="Example timeline
* for a live stream with multiple periods"> This case arises when a live stream is explicitly
* 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
* span more than one period. Multiple periods are also possible in the indefinite availability
* case.
*
* <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
* 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
......@@ -97,10 +97,10 @@ import com.google.android.exoplayer2.util.Assertions;
*
* <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
* for an on-demand stream with mid-roll ad groups"> This case includes mid-roll ad groups, which
* are defined as part of the timeline's single period. The period can be queried for information
* about the ad groups and the ads they contain.
* <p style="align:center"><img src="doc-files/timeline-single-file-midrolls.svg" alt="Example
* timeline for an on-demand stream with mid-roll ad groups"> This case includes mid-roll ad groups,
* which are defined as part of the timeline's single period. The period can be queried for
* information about the ad groups and the ads they contain.
*/
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
* 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">
*/
public static final class Window {
......@@ -284,12 +284,12 @@ public abstract class Timeline {
* 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,
* 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.
* <p align="center">
* <img src="doc-files/timeline-period.svg" alt="Information defined by a period">
* </p>
*
* <p style="align:center"><img src="doc-files/timeline-period.svg" alt="Information defined by a
* period">
*/
public static final class Period {
......
......@@ -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
* definition in ETSI TS 102 366 V1.2.1.
* definition in ETSI TS 102 366 V1.4.1.
*/
public final class Ac3Util {
......@@ -39,8 +39,8 @@ public final class Ac3Util {
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},
* {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}.
* AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link
* #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
......@@ -114,9 +114,7 @@ public final class Ac3Util {
* The number of new samples per (E-)AC-3 audio block.
*/
private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256;
/**
* Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1.
*/
/** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 4.1. */
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.
......@@ -134,20 +132,21 @@ public final class Ac3Util {
* Channel counts, indexed by acmod.
*/
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 ETSI TS 102 366 table 4.13.)
*/
private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96,
112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640};
/**
* 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[] {69, 87, 104,
121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393};
/** Nominal bitrates in kbps, indexed by frmsizecod / 2. (See table 4.13.) */
private static final int[] BITRATE_BY_HALF_FRMSIZECOD =
new int[] {
32, 40, 48, 56, 64, 80, 96, 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.) */
private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 =
new int[] {
69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253,
1393
};
/**
* Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to ETSI TS
* 102 366 Annex F. The reading position of {@code data} will be modified.
* Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to Annex F.
* The reading position of {@code data} will be modified.
*
* @param data The AC3SpecificBox to parse.
* @param trackId The track identifier to set on the format.
......@@ -179,8 +178,8 @@ public final class Ac3Util {
}
/**
* Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to ETSI TS
* 102 366 Annex F. The reading position of {@code data} will be modified.
* Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to Annex
* F. The reading position of {@code data} will be modified.
*
* @param data The EC3SpecificBox to parse.
* @param trackId The track identifier to set on the format.
......@@ -243,9 +242,10 @@ public final class Ac3Util {
public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) {
int initialPosition = data.getPosition();
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);
String mimeType;
@Nullable String mimeType;
@StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED;
int sampleRate;
int acmod;
......@@ -254,7 +254,7 @@ public final class Ac3Util {
boolean lfeon;
int channelCount;
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
switch (data.readBits(2)) { // strmtyp
case 0:
......@@ -472,7 +472,8 @@ public final class Ac3Util {
if (data.length < 6) {
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) {
int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits.
frmsiz |= data[3] & 0xFF; // Least significant 8 bits.
......@@ -485,24 +486,22 @@ public final class Ac3Util {
}
/**
* Returns the number of audio samples in an AC-3 syncframe.
*/
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
* Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's
* position is not modified.
*
* @param buffer The {@link ByteBuffer} from which to read the syncframe.
* @return The number of audio samples represented by the syncframe.
*/
public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) {
// See ETSI TS 102 366 subsection E.1.2.2.
int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6;
return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6
: BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]);
public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) {
// 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 numblkscod = fscod == 0x03 ? 3 : (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 {
* contain the start of a syncframe.
*/
public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) {
// TODO: Link to specification if available.
// The syncword ends 0xBA for TrueHD or 0xBB for MLP.
// See "Dolby TrueHD (MLP) high-level bitstream description" on the Dolby developer site,
// subsections 2.2 and 4.2.1. The syncword ends 0xBA for TrueHD or 0xBB for MLP.
if (syncframe[4] != (byte) 0xF8
|| syncframe[5] != (byte) 0x72
|| syncframe[6] != (byte) 0x6F
......
......@@ -58,6 +58,11 @@ public final class Ac4Util {
// TODO: Parse AC-4 stream channel count.
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
* header size.
*/
......@@ -218,7 +223,7 @@ public final class Ac4Util {
/** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */
public static void getAc4SampleHeader(int size, ParsableByteArray buffer) {
// 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[1] = 0x40;
buffer.data[2] = (byte) 0xFF;
......
......@@ -28,6 +28,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
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.Log;
import com.google.android.exoplayer2.util.Util;
......@@ -1148,9 +1149,7 @@ public final class DefaultAudioSink implements AudioSink {
case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_FLOAT:
case C.ENCODING_PCM_MU_LAW:
case Format.NO_VALUE:
default:
throw new IllegalArgumentException();
......@@ -1158,22 +1157,26 @@ public final class DefaultAudioSink implements AudioSink {
}
private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) {
return DtsUtil.parseDtsAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_AC3) {
return Ac3Util.getAc3SyncframeAudioSampleCount();
} else if (encoding == C.ENCODING_E_AC3 || encoding == C.ENCODING_E_AC3_JOC) {
return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_AC4) {
return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);
} else if (encoding == C.ENCODING_DOLBY_TRUEHD) {
int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer);
return syncframeOffset == C.INDEX_UNSET
? 0
: (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset)
* Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT);
} else {
throw new IllegalStateException("Unexpected audio encoding: " + encoding);
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);
case C.ENCODING_AC3:
case C.ENCODING_E_AC3:
case C.ENCODING_E_AC3_JOC:
return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer);
case C.ENCODING_AC4:
return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer);
case C.ENCODING_DOLBY_TRUEHD:
int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer);
return syncframeOffset == C.INDEX_UNSET
? 0
: (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset)
* Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT);
default:
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
private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10;
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 EventDispatcher eventDispatcher;
......@@ -566,7 +571,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
mediaFormat.getString(MediaFormat.KEY_MIME));
} else {
mediaFormat = outputMediaFormat;
encoding = getPcmEncoding(inputFormat);
if (outputMediaFormat.containsKey(VIVO_BITS_PER_SAMPLE_KEY)) {
encoding = Util.getPcmEncoding(outputMediaFormat.getInteger(VIVO_BITS_PER_SAMPLE_KEY));
} else {
encoding = getPcmEncoding(inputFormat);
}
}
int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
......
......@@ -29,8 +29,11 @@ import java.nio.ByteBuffer;
public AudioFormat onConfigure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException {
@C.PcmEncoding int encoding = inputAudioFormat.encoding;
if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {
if (encoding != C.ENCODING_PCM_8BIT
&& 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);
}
return encoding != C.ENCODING_PCM_16BIT
......@@ -50,6 +53,9 @@ import java.nio.ByteBuffer;
case C.ENCODING_PCM_8BIT:
resampledSize = size * 2;
break;
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
resampledSize = size;
break;
case C.ENCODING_PCM_24BIT:
resampledSize = (size / 3) * 2;
break;
......@@ -58,8 +64,6 @@ import java.nio.ByteBuffer;
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_FLOAT:
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_MU_LAW:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
......@@ -70,21 +74,28 @@ import java.nio.ByteBuffer;
ByteBuffer buffer = replaceOutputBuffer(resampledSize);
switch (inputAudioFormat.encoding) {
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++) {
buffer.put((byte) 0);
buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128));
}
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:
// 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) {
buffer.put(inputBuffer.get(i + 1));
buffer.put(inputBuffer.get(i + 2));
}
break;
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) {
buffer.put(inputBuffer.get(i + 2));
buffer.put(inputBuffer.get(i + 3));
......@@ -92,8 +103,6 @@ import java.nio.ByteBuffer;
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_FLOAT:
case C.ENCODING_PCM_A_LAW:
case C.ENCODING_PCM_MU_LAW:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
......
......@@ -97,6 +97,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private final AudioSink audioSink;
private final DecoderInputBuffer flagsOnlyBuffer;
private boolean drmResourcesAcquired;
private DecoderCounters decoderCounters;
private Format inputFormat;
private int encoderDelay;
......@@ -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
* 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() {
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);
}
protected abstract Format getOutputFormat();
/**
* Returns whether the existing decoder can be kept for a new format.
......@@ -546,6 +540,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
if (drmSessionManager != null && !drmResourcesAcquired) {
drmResourcesAcquired = true;
drmSessionManager.prepare();
}
decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
......@@ -595,6 +593,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
@Override
protected void onReset() {
if (drmSessionManager != null && drmResourcesAcquired) {
drmResourcesAcquired = false;
drmSessionManager.release();
}
}
@Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
switch (messageType) {
case C.MSG_SET_VOLUME:
......
......@@ -176,7 +176,7 @@ public final class TeeAudioProcessor extends BaseAudioProcessor {
// Write the rest of the header as little endian data.
scratchByteBuffer.clear();
scratchByteBuffer.putInt(16);
scratchByteBuffer.putShort((short) WavUtil.getTypeForEncoding(encoding));
scratchByteBuffer.putShort((short) WavUtil.getTypeForPcmEncoding(encoding));
scratchByteBuffer.putShort((short) channelCount);
scratchByteBuffer.putInt(sampleRateHz);
int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount);
......
......@@ -32,28 +32,33 @@ public final class WavUtil {
public static final int DATA_FOURCC = 0x64617461;
/** 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. */
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. */
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. */
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. */
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) {
switch (encoding) {
/**
* Returns the WAVE format type value for the given {@link C.PcmEncoding}.
*
* @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_16BIT:
case C.ENCODING_PCM_24BIT:
case C.ENCODING_PCM_32BIT:
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:
return TYPE_FLOAT;
case C.ENCODING_INVALID:
......@@ -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) {
case TYPE_PCM:
case TYPE_WAVE_FORMAT_EXTENSIBLE:
return Util.getPcmEncoding(bitsPerSample);
case TYPE_FLOAT:
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:
return C.ENCODING_INVALID;
}
......
......@@ -24,7 +24,6 @@ import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
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
......@@ -48,15 +47,11 @@ public abstract class BinarySearchSeeker {
*
* @param input The {@link ExtractorInput} from which data should be peeked.
* @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.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
TimestampSearchResult searchForTimestamp(
ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder)
TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
throws IOException, InterruptedException;
/** Called when a seek operation finishes. */
......@@ -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
* timestamp for a seek time position.
*/
......@@ -189,15 +167,11 @@ public abstract class BinarySearchSeeker {
* @param input The {@link ExtractorInput} from which data should be read.
* @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated
* 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}.
* @throws IOException If an error occurred reading from the input.
* @throws InterruptedException If the thread was interrupted.
*/
public int handlePendingSeek(
ExtractorInput input, PositionHolder seekPositionHolder, OutputFrameHolder outputFrameHolder)
public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder)
throws InterruptedException, IOException {
TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker);
while (true) {
......@@ -217,8 +191,7 @@ public abstract class BinarySearchSeeker {
input.resetPeekPosition();
TimestampSearchResult timestampSearchResult =
timestampSeeker.searchForTimestamp(
input, seekOperationParams.getTargetTimePosition(), outputFrameHolder);
timestampSeeker.searchForTimestamp(input, seekOperationParams.getTargetTimePosition());
switch (timestampSearchResult.type) {
case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED:
......@@ -419,7 +392,7 @@ public abstract class BinarySearchSeeker {
/**
* Represents possible search results for {@link
* TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}.
* TimestampSeeker#searchForTimestamp(ExtractorInput, long)}.
*/
public static final class TimestampSearchResult {
......@@ -495,10 +468,6 @@ public abstract class BinarySearchSeeker {
/**
* Returns a result to signal that the target timestamp has been found at {@code
* 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) {
return new TimestampSearchResult(
......
......@@ -58,7 +58,9 @@ public final class DefaultExtractorInput implements ExtractorInput {
public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {
int bytesRead = readFromPeekBuffer(target, offset, length);
if (bytesRead == 0) {
bytesRead = readFromDataSource(target, offset, length, 0, true);
bytesRead =
readFromDataSource(
target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true);
}
commitBytesRead(bytesRead);
return bytesRead;
......@@ -111,6 +113,31 @@ public final class DefaultExtractorInput implements ExtractorInput {
}
@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)
throws IOException, InterruptedException {
if (!advancePeekPosition(length, allowEndOfInput)) {
......@@ -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 offset The offset into the target array at which to write.
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor;
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.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
......@@ -55,15 +56,24 @@ import java.lang.reflect.Constructor;
*/
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 {
Constructor<? extends Extractor> flacExtractorConstructor = null;
Constructor<? extends Extractor> flacExtensionExtractorConstructor = null;
try {
// LINT.IfChange
flacExtractorConstructor =
Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor")
.asSubclass(Extractor.class)
.getConstructor();
@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")
.asSubclass(Extractor.class)
.getConstructor();
}
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
} catch (ClassNotFoundException e) {
// Expected if the app was built without the FLAC extension.
......@@ -71,7 +81,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
// The FLAC extension is present, but instantiation failed.
throw new RuntimeException("Error instantiating FLAC extension", e);
}
FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor;
FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR = flacExtensionExtractorConstructor;
}
private boolean constantBitrateSeekingEnabled;
......@@ -208,7 +218,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Override
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[1] = new FragmentedMp4Extractor(fragmentedMp4Flags);
extractors[2] = new Mp4Extractor(mp4Flags);
......@@ -237,13 +247,19 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
: 0));
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 {
extractors[13] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance();
extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance();
} catch (Exception e) {
// Should never happen.
throw new IllegalStateException("Unexpected error creating FLAC extractor", e);
}
} else {
extractors[13] = new FlacExtractor();
}
return extractors;
}
......
......@@ -27,19 +27,19 @@ import java.io.InputStream;
* for more info about each mode.
*
* <ul>
* <li>The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level
* access operations.
* <li>The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like
* byte-level access operations.
* <li>The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user
* wants to read an entire block/frame/header of known length.
* </ul>
*
* <h3>{@link InputStream}-like methods</h3>
*
* <p>The {@code read()} and {@code skip()} methods provide {@link InputStream}-like byte-level
* access operations. The {@code length} parameter is a maximum, and each method returns the number
* of bytes actually processed. This may be less than {@code length} because the end of the input
* was reached, or the method was interrupted, or the operation was aborted early for another
* reason.
* <p>The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like
* byte-level access operations. The {@code length} parameter is a maximum, and each method returns
* the number of bytes actually processed. This may be less than {@code length} because the end of
* the input was reached, or the method was interrupted, or the operation was aborted early for
* another reason.
*
* <h3>Block-based methods</h3>
*
......@@ -102,7 +102,8 @@ public interface ExtractorInput {
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 offset The offset into the target array at which to write.
......@@ -155,8 +156,11 @@ public interface ExtractorInput {
void skipFully(int length) throws IOException, InterruptedException;
/**
* Peeks {@code length} bytes from the peek position, writing them into {@code target} at index
* {@code offset}. The current read position is left unchanged.
* Peeks up to {@code length} bytes from the peek position. The current read position is left
* 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
* position, so the caller can peek the same data again. Reading or skipping also resets the peek
......@@ -164,6 +168,18 @@ public interface ExtractorInput {
*
* @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, 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 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
......@@ -181,12 +197,8 @@ public interface ExtractorInput {
throws IOException, InterruptedException;
/**
* Peeks {@code length} bytes from the peek position, writing them into {@code target} at index
* {@code offset}. The current read position is left unchanged.
* <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.
* Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(target, offset, length,
* false)}.
*
* @param target A target array into which data should be written.
* @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 {
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
* is invalid.
*/
public static int getFrameSize(int header) {
if ((header & 0xFFE00000) != 0xFFE00000) {
if (!isMagicPresent(header)) {
return C.LENGTH_UNSET;
}
......@@ -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.
*
* @param headerData Header data to parse.
......@@ -129,7 +164,7 @@ public final class MpegAudioHeader {
* is not a valid MPEG audio header.
*/
public static boolean populateHeader(int headerData, MpegAudioHeader header) {
if ((headerData & 0xFFE00000) != 0xFFE00000) {
if (!isMagicPresent(headerData)) {
return false;
}
......@@ -166,23 +201,20 @@ public final class MpegAudioHeader {
int padding = (headerData >>> 9) & 1;
int bitrate;
int frameSize;
int samplesPerFrame;
int samplesPerFrame = getFrameSizeInSamples(version, layer);
if (layer == 3) {
// Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
frameSize = (12 * bitrate / sampleRate + padding) * 4;
samplesPerFrame = 384;
} else {
// Layer II (layer == 2) or III (layer == 1)
if (version == 3) {
// Version 1
bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
samplesPerFrame = 1152;
frameSize = 144 * bitrate / sampleRate + padding;
} else {
// Version 2 or 2.5.
bitrate = BITRATE_V2[bitrateIndex - 1];
samplesPerFrame = layer == 1 ? 576 : 1152;
frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding;
}
}
......@@ -193,6 +225,22 @@ public final class MpegAudioHeader {
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. */
public int version;
/** The mime type. */
......
......@@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.ogg;
package com.google.android.exoplayer2.extractor;
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
* specification</a>
*/
/* package */ final class VorbisBitArray {
public final class VorbisBitArray {
private final byte[] data;
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;
} else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
: MimeTypes.AUDIO_MLAW;
int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT;
Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE,
Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null);
Format format =
Format.createAudioSampleFormat(
/* 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);
hasOutputFormat = true;
} else if (audioFormat != AUDIO_FORMAT_AAC) {
......
......@@ -1248,10 +1248,10 @@ public class MatroskaExtractor implements Extractor {
if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) {
if (blockSampleCount > 1) {
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.");
} 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
// appropriate encryption data here.
track.output.sampleData(subtitleSample, subtitleSample.limit());
......@@ -1827,10 +1827,8 @@ public class MatroskaExtractor implements Extractor {
chunkSize += size;
chunkOffset = offset; // The offset is to the end of the sample.
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) {
......
......@@ -379,6 +379,9 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_dfLa = 0x64664c61;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_twos = 0x74776f73;
public final int type;
public Atom(int type) {
......
......@@ -779,6 +779,7 @@ import java.util.List;
|| childAtomType == Atom.TYPE_sawb
|| childAtomType == Atom.TYPE_lpcm
|| childAtomType == Atom.TYPE_sowt
|| childAtomType == Atom.TYPE_twos
|| childAtomType == Atom.TYPE__mp3
|| childAtomType == Atom.TYPE_alac
|| childAtomType == Atom.TYPE_alaw
......@@ -1039,6 +1040,7 @@ import java.util.List;
int channelCount;
int sampleRate;
@C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
channelCount = parent.readUnsignedShort();
......@@ -1099,6 +1101,10 @@ import java.util.List;
mimeType = MimeTypes.AUDIO_AMR_WB;
} else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) {
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) {
mimeType = MimeTypes.AUDIO_MPEG;
} else if (atomType == Atom.TYPE_alac) {
......@@ -1185,9 +1191,6 @@ import java.util.List;
}
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,
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,
initializationData == null ? null : Collections.singletonList(initializationData),
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp4;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
......@@ -74,33 +75,206 @@ import java.nio.ByteBuffer;
private static final int PICTURE_TYPE_FRONT_COVER = 3;
// Standard genres.
private static final String[] STANDARD_GENRES = new String[] {
// These are the official ID3v1 genres.
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz",
"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",
// These were made up by the authors of Winamp and later added to the ID3 spec.
"Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
"Celtic", "Bluegrass", "Avantgarde", "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",
// These were med up by the authors of Winamp but have not been added to the ID3 spec.
"Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk",
"Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover",
"Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime",
"Jpop", "Synthpop"
};
@VisibleForTesting
/* package */ static final String[] STANDARD_GENRES =
new String[] {
// These are the official ID3v1 genres.
"Blues",
"Classic Rock",
"Country",
"Dance",
"Disco",
"Funk",
"Grunge",
"Hip-Hop",
"Jazz",
"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",
// 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",
"Celtic",
"Bluegrass",
"Avantgarde",
"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";
......
......@@ -108,9 +108,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private ParsableByteArray atomData;
private int sampleTrackIndex;
private int sampleBytesRead;
private int sampleBytesWritten;
private int sampleCurrentNalBytesRemaining;
private boolean isAc4HeaderRequired;
// Extractor outputs.
private ExtractorOutput extractorOutput;
......@@ -158,9 +158,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
containerAtoms.clear();
atomHeaderBytesRead = 0;
sampleTrackIndex = C.INDEX_UNSET;
sampleBytesRead = 0;
sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0;
isAc4HeaderRequired = false;
if (position == 0) {
enterReadingAtomHeaderState();
} else if (tracks != null) {
......@@ -501,15 +501,13 @@ public final class Mp4Extractor implements Extractor, SeekMap {
if (sampleTrackIndex == C.INDEX_UNSET) {
return RESULT_END_OF_INPUT;
}
isAc4HeaderRequired =
MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType);
}
Mp4Track track = tracks[sampleTrackIndex];
TrackOutput trackOutput = track.trackOutput;
int sampleIndex = track.sampleIndex;
long position = track.sampleTable.offsets[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) {
positionHolder.position = position;
return RESULT_SEEK;
......@@ -537,6 +535,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
if (sampleCurrentNalBytesRemaining == 0) {
// Read the NAL length so that we know where we find the next one.
input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
sampleBytesRead += nalUnitLengthFieldLength;
nalLength.setPosition(0);
int nalLengthInt = nalLength.readInt();
if (nalLengthInt < 0) {
......@@ -551,21 +550,23 @@ public final class Mp4Extractor implements Extractor, SeekMap {
} else {
// Write the payload of the NAL unit.
int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false);
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes;
}
}
} else {
if (isAc4HeaderRequired) {
Ac4Util.getAc4SampleHeader(sampleSize, scratch);
int length = scratch.limit();
trackOutput.sampleData(scratch, length);
sampleSize += length;
sampleBytesWritten += length;
isAc4HeaderRequired = false;
if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) {
if (sampleBytesWritten == 0) {
Ac4Util.getAc4SampleHeader(sampleSize, scratch);
trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE);
sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE;
}
sampleSize += Ac4Util.SAMPLE_HEADER_SIZE;
}
while (sampleBytesWritten < sampleSize) {
int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false);
sampleBytesRead += writtenBytes;
sampleBytesWritten += writtenBytes;
sampleCurrentNalBytesRemaining -= writtenBytes;
}
......@@ -574,6 +575,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
track.sampleIndex++;
sampleTrackIndex = C.INDEX_UNSET;
sampleBytesRead = 0;
sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0;
return RESULT_CONTINUE;
......
......@@ -15,18 +15,18 @@
*/
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.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.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.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* {@link StreamReader} to extract Flac data out of Ogg byte stream.
......@@ -34,7 +34,6 @@ import java.util.List;
/* package */ final class FlacReader extends StreamReader {
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;
......@@ -72,27 +71,13 @@ import java.util.List;
byte[] data = packet.data;
if (streamMetadata == null) {
streamMetadata = new FlacStreamMetadata(data, 17);
int maxInputSize =
streamMetadata.maxFrameSize == 0 ? Format.NO_VALUE : streamMetadata.maxFrameSize;
byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
List<byte[]> initializationData = Collections.singletonList(metadata);
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) {
setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null);
} else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) {
flacOggSeeker = new FlacOggSeeker();
flacOggSeeker.parseSeekTable(packet);
FlacStreamMetadata.SeekTable seekTable =
FlacMetadataReader.readSeekTableMetadataBlock(packet);
streamMetadata = streamMetadata.copyWithSeekTable(seekTable);
} else if (isAudioPacket(data)) {
if (flacOggSeeker != null) {
flacOggSeeker.setFirstFrameOffset(position);
......@@ -104,44 +89,19 @@ import java.util.List;
}
private int getFlacFrameBlockSize(ParsableByteArray packet) {
int blockSizeCode = (packet.data[2] & 0xFF) >> 4;
switch (blockSizeCode) {
case 1:
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.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 blockSizeKey = (packet.data[2] & 0xFF) >> 4;
if (blockSizeKey == 6 || blockSizeKey == 7) {
// Skip the sample number.
packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
packet.readUtf8EncodedLong();
}
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 pendingSeekGranule;
......@@ -154,27 +114,6 @@ import java.util.List;
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
public long read(ExtractorInput input) throws IOException, InterruptedException {
if (pendingSeekGranule >= 0) {
......@@ -187,40 +126,16 @@ import java.util.List;
@Override
public void startSeek(long targetGranule) {
Assertions.checkNotNull(streamMetadata.seekTable);
long[] seekPointGranules = streamMetadata.seekTable.pointSampleNumbers;
int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true);
pendingSeekGranule = seekPointGranules[index];
}
@Override
public SeekMap createSeekMap() {
return this;
}
@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();
Assertions.checkState(firstFrameOffset != -1);
return new FlacSeekTableSeekMap(streamMetadata, firstFrameOffset);
}
}
......
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