ExoPlayerUtils.java

/*
 * Copyright 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 androidx.media2.player;

import static android.media.MediaFormat.MIMETYPE_TEXT_CEA_608;
import static android.media.MediaFormat.MIMETYPE_TEXT_CEA_708;

import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO;
import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA;
import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE;
import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN;
import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO;
import static androidx.media2.player.MediaPlayer2.MEDIA_ERROR_IO;
import static androidx.media2.player.MediaPlayer2.MEDIA_ERROR_MALFORMED;
import static androidx.media2.player.MediaPlayer2.MEDIA_ERROR_TIMED_OUT;
import static androidx.media2.player.MediaPlayer2.MEDIA_ERROR_UNKNOWN;

import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.media.MediaFormat;
import android.net.Uri;

import androidx.core.util.Preconditions;
import androidx.media.AudioAttributesCompat;
import androidx.media2.common.CallbackMediaItem;
import androidx.media2.common.FileMediaItem;
import androidx.media2.common.MediaItem;
import androidx.media2.common.UriMediaItem;
import androidx.media2.exoplayer.external.C;
import androidx.media2.exoplayer.external.ExoPlaybackException;
import androidx.media2.exoplayer.external.Format;
import androidx.media2.exoplayer.external.ParserException;
import androidx.media2.exoplayer.external.PlaybackParameters;
import androidx.media2.exoplayer.external.SeekParameters;
import androidx.media2.exoplayer.external.audio.AudioAttributes;
import androidx.media2.exoplayer.external.extractor.DefaultExtractorsFactory;
import androidx.media2.exoplayer.external.extractor.ExtractorsFactory;
import androidx.media2.exoplayer.external.extractor.ts.AdtsExtractor;
import androidx.media2.exoplayer.external.mediacodec.MediaFormatUtil;
import androidx.media2.exoplayer.external.source.ExtractorMediaSource;
import androidx.media2.exoplayer.external.source.MediaSource;
import androidx.media2.exoplayer.external.source.hls.HlsMediaSource;
import androidx.media2.exoplayer.external.upstream.DataSource;
import androidx.media2.exoplayer.external.upstream.HttpDataSource;
import androidx.media2.exoplayer.external.upstream.RawResourceDataSource;
import androidx.media2.exoplayer.external.util.MimeTypes;
import androidx.media2.exoplayer.external.util.Util;

import java.io.IOException;
import java.net.SocketTimeoutException;

/**
 * Utility methods for translating between the MediaPlayer2 and ExoPlayer APIs.
 */
@SuppressLint("RestrictedApi") // TODO(b/68398926): Remove once RestrictedApi checks are fixed.
/* package */ class ExoPlayerUtils {

    private static final ExtractorsFactory sExtractorsFactory = new DefaultExtractorsFactory()
            .setAdtsExtractorFlags(AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);

    /**
     * Returns an ExoPlayer media source for the given media item. The given {@link MediaItem} is
     * set as the tag of the source.
     */
    public static MediaSource createUnclippedMediaSource(
            Context context, DataSource.Factory dataSourceFactory, MediaItem mediaItem) {
        if (mediaItem instanceof UriMediaItem) {
            Uri uri = ((UriMediaItem) mediaItem).getUri();
            if (Util.inferContentType(uri) == C.TYPE_HLS) {
                return new HlsMediaSource.Factory(dataSourceFactory)
                        .setTag(mediaItem)
                        .createMediaSource(uri);
            } else {
                if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) {
                    String path = Preconditions.checkNotNull(uri.getPath());
                    int resourceIdentifier;
                    if (uri.getPathSegments().size() == 1
                            && uri.getPathSegments().get(0).matches("\d+")) {
                        resourceIdentifier = Integer.parseInt(uri.getPathSegments().get(0));
                    } else {
                        path = path.replaceAll("^/", "");
                        String host = uri.getHost();
                        String resourceName = (host != null ? host + ":" : "") + path;
                        resourceIdentifier = context.getResources()
                                .getIdentifier(resourceName, "raw", context.getPackageName());
                    }
                    Preconditions.checkState(resourceIdentifier != 0);
                    uri = RawResourceDataSource.buildRawResourceUri(resourceIdentifier);
                }
                return new ExtractorMediaSource.Factory(dataSourceFactory)
                        .setExtractorsFactory(sExtractorsFactory)
                        .setTag(mediaItem)
                        .createMediaSource(uri);
            }
        } else if (mediaItem instanceof FileMediaItem) {
            return new ExtractorMediaSource.Factory(dataSourceFactory)
                    .setExtractorsFactory(sExtractorsFactory)
                    .setTag(mediaItem)
                    .createMediaSource(Uri.EMPTY);
        } else if (mediaItem instanceof CallbackMediaItem) {
            CallbackMediaItem callbackMediaItem = (CallbackMediaItem) mediaItem;
            dataSourceFactory = DataSourceCallbackDataSource.getFactory(
                    callbackMediaItem.getDataSourceCallback());
            return new ExtractorMediaSource.Factory(dataSourceFactory)
                    .setExtractorsFactory(sExtractorsFactory)
                    .setTag(mediaItem)
                    .createMediaSource(Uri.EMPTY);
        } else {
            throw new IllegalStateException();
        }
    }

    /** Returns ExoPlayer audio attributes for the given audio attributes. */
    public static AudioAttributes getAudioAttributes(AudioAttributesCompat audioAttributesCompat) {
        return new AudioAttributes.Builder()
                .setContentType(audioAttributesCompat.getContentType())
                .setFlags(audioAttributesCompat.getFlags())
                .setUsage(audioAttributesCompat.getUsage())
                .build();
    }

    /** Returns audio attributes for the given ExoPlayer audio attributes. */
    public static AudioAttributesCompat getAudioAttributesCompat(AudioAttributes audioAttributes) {
        return new AudioAttributesCompat.Builder()
                .setContentType(audioAttributes.contentType)
                .setFlags(audioAttributes.flags)
                .setUsage(audioAttributes.usage)
                .build();
    }

    /** Returns ExoPlayer playback parameters for the given playback params. */
    public static PlaybackParameters getPlaybackParameters(PlaybackParams playbackParams2) {
        Float speed = playbackParams2.getSpeed();
        Float pitch = playbackParams2.getPitch();
        return new PlaybackParameters(speed != null ? speed : 1f, pitch != null ? pitch : 1f);
    }

    /** Returns the ExoPlayer seek parameters corresponding to the given seek mode. */
    public static SeekParameters getSeekParameters(int seekMode) {
        switch (seekMode) {
            case MediaPlayer2.SEEK_CLOSEST:
                return SeekParameters.EXACT;
            case MediaPlayer2.SEEK_CLOSEST_SYNC:
                return SeekParameters.CLOSEST_SYNC;
            case MediaPlayer2.SEEK_NEXT_SYNC:
                return SeekParameters.NEXT_SYNC;
            case MediaPlayer2.SEEK_PREVIOUS_SYNC:
                return SeekParameters.PREVIOUS_SYNC;
            default:
                throw new IllegalArgumentException();
        }
    }

    /** Returns the MEDIA_ERROR_* constant for an ExoPlayer player exception. */
    public static int getError(ExoPlaybackException exception) {
        if (exception.type == ExoPlaybackException.TYPE_SOURCE) {
            IOException sourceException = exception.getSourceException();
            if (sourceException instanceof ParserException) {
                return MEDIA_ERROR_MALFORMED;
            } else {
                if (sourceException instanceof HttpDataSource.HttpDataSourceException
                        && sourceException.getCause() instanceof SocketTimeoutException) {
                    return MEDIA_ERROR_TIMED_OUT;
                }
                return MEDIA_ERROR_IO;
            }
        }
        return MEDIA_ERROR_UNKNOWN;
    }

    /** Returns the ExoPlayer track type for the given track type. */
    public static int getExoPlayerTrackType(int trackType) {
        switch (trackType) {
            case MEDIA_TRACK_TYPE_AUDIO:
                return C.TRACK_TYPE_AUDIO;
            case MEDIA_TRACK_TYPE_VIDEO:
                return C.TRACK_TYPE_VIDEO;
            case MEDIA_TRACK_TYPE_SUBTITLE:
                return C.TRACK_TYPE_TEXT;
            case MEDIA_TRACK_TYPE_METADATA:
                return C.TRACK_TYPE_METADATA;
            case MEDIA_TRACK_TYPE_UNKNOWN:
            default:
                return C.TRACK_TYPE_UNKNOWN;
        }
    }

    /** Returns the track type corresponding to the given ExoPlayer track type. */
    public static int getTrackType(int exoPlayerTrackType) {
        switch (exoPlayerTrackType) {
            case C.TRACK_TYPE_AUDIO:
                return MEDIA_TRACK_TYPE_AUDIO;
            case C.TRACK_TYPE_VIDEO:
                return MEDIA_TRACK_TYPE_VIDEO;
            case C.TRACK_TYPE_TEXT:
                return MEDIA_TRACK_TYPE_SUBTITLE;
            case C.TRACK_TYPE_METADATA:
                return MEDIA_TRACK_TYPE_METADATA;
            case C.TRACK_TYPE_NONE:
            case C.TRACK_TYPE_CAMERA_MOTION:
            case C.TRACK_TYPE_DEFAULT:
            case C.TRACK_TYPE_UNKNOWN:
            default:
                return MEDIA_TRACK_TYPE_UNKNOWN;
        }
    }

    /** Returns the media format corresponding to an ExoPlayer format. */
    @SuppressLint("InlinedApi")
    public static MediaFormat getMediaFormat(Format format) {
        MediaFormat mediaFormat = new MediaFormat();
        String mimeType = format.sampleMimeType;
        mediaFormat.setString(MediaFormat.KEY_MIME, mimeType);
        int trackType = MimeTypes.getTrackType(mimeType);
        if (trackType == C.TRACK_TYPE_AUDIO) {
            mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, format.channelCount);
            mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, format.sampleRate);
            if (format.language != null) {
                mediaFormat.setString(MediaFormat.KEY_LANGUAGE, format.language);
            }
        } else if (trackType == C.TRACK_TYPE_VIDEO) {
            MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_WIDTH, format.width);
            MediaFormatUtil.maybeSetInteger(mediaFormat, MediaFormat.KEY_HEIGHT, format.height);
            MediaFormatUtil.maybeSetFloat(
                    mediaFormat, MediaFormat.KEY_FRAME_RATE, format.frameRate);
            MediaFormatUtil.maybeSetInteger(
                    mediaFormat, MediaFormat.KEY_ROTATION, format.rotationDegrees);
            MediaFormatUtil.maybeSetColorInfo(mediaFormat, format.colorInfo);
        } else if (trackType == C.TRACK_TYPE_TEXT) {
            boolean isAutoselect = format.selectionFlags == C.SELECTION_FLAG_AUTOSELECT;
            boolean isDefault = format.selectionFlags == C.SELECTION_FLAG_DEFAULT;
            boolean isForced = format.selectionFlags == C.SELECTION_FLAG_FORCED;
            mediaFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, isAutoselect ? 1 : 0);
            mediaFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, isDefault ? 1 : 0);
            mediaFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, isForced ? 1 : 0);
            if (format.language == null) {
                mediaFormat.setString(MediaFormat.KEY_LANGUAGE, C.LANGUAGE_UNDETERMINED);
            } else {
                mediaFormat.setString(MediaFormat.KEY_LANGUAGE, format.language);
            }
            // MediaPlayer2 uses text/* instead of application/* MIME types.
            if (MimeTypes.APPLICATION_CEA608.equals(mimeType)) {
                mediaFormat.setString(MediaFormat.KEY_MIME, MIMETYPE_TEXT_CEA_608);
            } else if (MimeTypes.APPLICATION_CEA708.equals(mimeType)) {
                mediaFormat.setString(MediaFormat.KEY_MIME, MIMETYPE_TEXT_CEA_708);
            }
        }
        return mediaFormat;
    }

    private ExoPlayerUtils() {
        // Prevent instantiation.
    }

}