/*
* Copyright (C) 2014 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.exoplayer.chunk;
import com.google.android.exoplayer.MediaCodecUtil;
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Point;
import android.view.Display;
import android.view.WindowManager;
import java.util.ArrayList;
import java.util.List;
/**
* Selects from possible video formats.
*/
public final class VideoFormatSelectorUtil {
/**
* If a dimension (i.e. width or height) of a video is greater or equal to this fraction of the
* corresponding viewport dimension, then the video is considered as filling the viewport (in that
* dimension).
*/
private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f;
/**
* Chooses a suitable subset from a number of video formats, to be rendered on the device's
* default display.
*
* @param context A context.
* @param formatWrappers Wrapped formats from which to select.
* @param allowedContainerMimeTypes An array of allowed container mime types. Null allows all
* mime types.
* @param filterHdFormats True to filter HD formats. False otherwise.
* @return An array holding the indices of the selected formats.
* @throws DecoderQueryException
*/
public static int[] selectVideoFormatsForDefaultDisplay(Context context,
List<? extends FormatWrapper> formatWrappers, String[] allowedContainerMimeTypes,
boolean filterHdFormats) throws DecoderQueryException {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
Point displaySize = getDisplaySize(display);
return selectVideoFormats(formatWrappers, allowedContainerMimeTypes, filterHdFormats, true,
displaySize.x, displaySize.y);
}
/**
* Chooses a suitable subset from a number of video formats.
* <p>
* A format is filtered (i.e. not selected) if:
* <ul>
* <li>{@code allowedContainerMimeTypes} is non-null and the format does not have one of the
* permitted mime types.
* <li>{@code filterHdFormats} is true and the format is HD.
* <li>It's determined that the video decoder isn't powerful enough to decode the format.
* <li>There exists another format of lower resolution whose resolution exceeds the maximum size
* in pixels that the video can be rendered within the viewport.
* </ul>
*
* @param formatWrappers Wrapped formats from which to select.
* @param allowedContainerMimeTypes An array of allowed container mime types. Null allows all
* mime types.
* @param filterHdFormats True to filter HD formats. False otherwise.
* @param orientationMayChange True if the video's orientation may change with respect to the
* viewport during playback.
* @param viewportWidth The width in pixels of the viewport within which the video will be
* displayed. If the viewport size may change, this should be set to the maximum possible
* width.
* @param viewportHeight The height in pixels of the viewport within which the video will be
* displayed. If the viewport size may change, this should be set to the maximum possible
* height.
* @return An array holding the indices of the selected formats.
* @throws DecoderQueryException
*/
public static int[] selectVideoFormats(List<? extends FormatWrapper> formatWrappers,
String[] allowedContainerMimeTypes, boolean filterHdFormats, boolean orientationMayChange,
int viewportWidth, int viewportHeight) throws DecoderQueryException {
int maxVideoPixelsToRetain = Integer.MAX_VALUE;
ArrayList<Integer> selectedIndexList = new ArrayList<>();
int maxDecodableFrameSize = MediaCodecUtil.maxH264DecodableFrameSize();
// First pass to filter out formats that individually fail to meet the selection criteria.
int formatWrapperCount = formatWrappers.size();
for (int i = 0; i < formatWrapperCount; i++) {
Format format = formatWrappers.get(i).getFormat();
if (isFormatPlayable(format, allowedContainerMimeTypes, filterHdFormats,
maxDecodableFrameSize)) {
// Select the format for now. It may still be filtered in the second pass below.
selectedIndexList.add(i);
// Keep track of the number of pixels of the selected format whose resolution is the
// smallest to exceed the maximum size at which it can be displayed within the viewport.
// We'll discard formats of higher resolution in a second pass.
if (format.width > 0 && format.height > 0) {
Point maxVideoSizeInViewport = getMaxVideoSizeInViewport(orientationMayChange,
viewportWidth, viewportHeight, format.width, format.height);
int videoPixels = format.width * format.height;
if (format.width >= (int) (maxVideoSizeInViewport.x * FRACTION_TO_CONSIDER_FULLSCREEN)
&& format.height >= (int) (maxVideoSizeInViewport.y * FRACTION_TO_CONSIDER_FULLSCREEN)
&& videoPixels < maxVideoPixelsToRetain) {
maxVideoPixelsToRetain = videoPixels;
}
}
}
}
// Second pass to filter out formats that exceed maxVideoPixelsToRetain. These formats are have
// unnecessarily high resolution given the size at which the video will be displayed within the
// viewport.
for (int i = selectedIndexList.size() - 1; i >= 0; i--) {
Format format = formatWrappers.get(i).getFormat();
if (format.width > 0 && format.height > 0
&& format.width * format.height > maxVideoPixelsToRetain) {
selectedIndexList.remove(i);
}
}
return Util.toArray(selectedIndexList);
}
/**
* Determines whether an individual format is playable, given an array of allowed container types,
* whether HD formats should be filtered and a maximum decodable frame size in pixels.
*/
private static boolean isFormatPlayable(Format format, String[] allowedContainerMimeTypes,
boolean filterHdFormats, int maxDecodableFrameSize) {
if (allowedContainerMimeTypes != null
&& !Util.contains(allowedContainerMimeTypes, format.mimeType)) {
// Filtering format based on its container mime type.
return false;
}
if (filterHdFormats && (format.width >= 1280 || format.height >= 720)) {
// Filtering format because it's HD.
return false;
}
if (format.width > 0 && format.height > 0) {
// TODO: Use MediaCodecUtil.isSizeAndRateSupportedV21 on API levels >= 21 if we know the
// mimeType of the media samples within the container. Remove the assumption that we're
// dealing with H.264.
if (format.width * format.height > maxDecodableFrameSize) {
// Filtering stream that device cannot play
return false;
}
}
return true;
}
/**
* Given viewport dimensions and video dimensions, computes the maximum size of the video as it
* will be rendered to fit inside of the viewport.
*/
private static Point getMaxVideoSizeInViewport(boolean orientationMayChange, int viewportWidth,
int viewportHeight, int videoWidth, int videoHeight) {
if (orientationMayChange && (videoWidth > videoHeight) != (viewportWidth > viewportHeight)) {
// Rotation is allowed, and the video will be larger in the rotated viewport.
int tempViewportWidth = viewportWidth;
viewportWidth = viewportHeight;
viewportHeight = tempViewportWidth;
}
if (videoWidth * viewportHeight >= videoHeight * viewportWidth) {
// Horizontal letter-boxing along top and bottom.
return new Point(viewportWidth, Util.ceilDivide(viewportWidth * videoHeight, videoWidth));
} else {
// Vertical letter-boxing along edges.
return new Point(Util.ceilDivide(viewportHeight * videoWidth, videoHeight), viewportHeight);
}
}
private static Point getDisplaySize(Display display) {
Point displaySize = new Point();
if (Util.SDK_INT >= 17) {
getDisplaySizeV17(display, displaySize);
} else if (Util.SDK_INT >= 16) {
getDisplaySizeV16(display, displaySize);
} else {
getDisplaySizeV9(display, displaySize);
}
return displaySize;
}
@TargetApi(17)
private static void getDisplaySizeV17(Display display, Point outSize) {
display.getRealSize(outSize);
}
@TargetApi(16)
private static void getDisplaySizeV16(Display display, Point outSize) {
display.getSize(outSize);
}
@SuppressWarnings("deprecation")
private static void getDisplaySizeV9(Display display, Point outSize) {
outSize.x = display.getWidth();
outSize.y = display.getHeight();
}
private VideoFormatSelectorUtil() {}
}