/* * Copyright (C) 2016 Brian Wernick * * 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.devbrackets.android.exomedia.ui.widget; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.support.annotation.DrawableRes; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewStub; import android.widget.ImageView; import android.widget.RelativeLayout; import com.devbrackets.android.exomedia.ExoMedia; import com.devbrackets.android.exomedia.R; import com.devbrackets.android.exomedia.core.ListenerMux; import com.devbrackets.android.exomedia.core.api.VideoViewApi; import com.devbrackets.android.exomedia.core.exoplayer.ExoMediaPlayer; import com.devbrackets.android.exomedia.core.listener.MetadataListener; import com.devbrackets.android.exomedia.core.video.exo.ExoTextureVideoView; import com.devbrackets.android.exomedia.core.video.mp.NativeTextureVideoView; import com.devbrackets.android.exomedia.core.video.scale.ScaleType; import com.devbrackets.android.exomedia.listener.OnBufferUpdateListener; import com.devbrackets.android.exomedia.listener.OnCompletionListener; import com.devbrackets.android.exomedia.listener.OnErrorListener; import com.devbrackets.android.exomedia.listener.OnPreparedListener; import com.devbrackets.android.exomedia.listener.OnSeekCompletionListener; import com.devbrackets.android.exomedia.listener.OnVideoSizeChangedListener; import com.devbrackets.android.exomedia.util.DeviceUtil; import com.devbrackets.android.exomedia.util.StopWatch; import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import java.util.Map; /** * This is a support VideoView that will use the standard VideoView on devices below * JellyBean. On devices with JellyBean and up we will use the ExoPlayer in order to * better support HLS streaming and full 1080p video resolutions which the VideoView * struggles with, and in some cases crashes. * <p> * To an external user this view should have the same APIs used with the standard VideoView * to help with quick implementations. */ @SuppressWarnings("UnusedDeclaration") public class VideoView extends RelativeLayout { private static final String TAG = VideoView.class.getSimpleName(); @Nullable protected VideoControls videoControls; protected ImageView previewImageView; protected Uri videoUri; protected VideoViewApi videoViewImpl; protected DeviceUtil deviceUtil = new DeviceUtil(); protected AudioManager audioManager; @NonNull protected AudioFocusHelper audioFocusHelper = new AudioFocusHelper(); protected long positionOffset = 0; protected long overriddenDuration = -1; protected boolean overridePosition = false; protected StopWatch overriddenPositionStopWatch = new StopWatch(); protected MuxNotifier muxNotifier = new MuxNotifier(); protected ListenerMux listenerMux; protected boolean releaseOnDetachFromWindow = true; protected boolean handleAudioFocus = true; public VideoView(Context context) { super(context); setup(context, null); } public VideoView(Context context, AttributeSet attrs) { super(context, attrs); setup(context, attrs); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public VideoView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setup(context, attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setup(context, attrs); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (!isInEditMode() && releaseOnDetachFromWindow) { release(); } } @Override public void setOnTouchListener(OnTouchListener listener) { videoViewImpl.setOnTouchListener(listener); super.setOnTouchListener(listener); } /** * <b><em>WARNING:</em></b> Use of this method may cause memory leaks. * <p> * Enables or disables the automatic release when the VideoView is detached * from the window. Normally this is expected to release all resources used * by calling {@link #release()}. If <code>releaseOnDetach</code> is disabled * then {@link #release()} will need to be manually called. * * @param releaseOnDetach False to disable the automatic release in {@link #onDetachedFromWindow()} */ public void setReleaseOnDetachFromWindow(boolean releaseOnDetach) { this.releaseOnDetachFromWindow = releaseOnDetach; } /** * Stops the playback and releases all resources attached to this * VideoView. This should not be called manually unless * {@link #setReleaseOnDetachFromWindow(boolean)} has been set. */ public void release() { videoControls = null; stopPlayback(); overriddenPositionStopWatch.stop(); videoViewImpl.release(); } /** * Sets an image that will be visible only when the video is loading. * * @param drawable The drawable to use for the preview image */ public void setPreviewImage(@Nullable Drawable drawable) { if (previewImageView != null) { previewImageView.setImageDrawable(drawable); } } /** * Sets an image that will be visible only when the video is loading. * * @param resourceId The resourceId representing the preview image */ public void setPreviewImage(@DrawableRes int resourceId) { if (previewImageView != null) { previewImageView.setImageResource(resourceId); } } /** * Sets an image that will be visible only when the video is loading. * * @param bitmap The bitmap to use for the preview image */ public void setPreviewImage(@Nullable Bitmap bitmap) { if (previewImageView != null) { previewImageView.setImageBitmap(bitmap); } } /** * Sets an image that will be visible only when the video is loading. * * @param uri The Uri pointing to the preview image */ public void setPreviewImage(@Nullable Uri uri) { if (previewImageView != null) { previewImageView.setImageURI(uri); } } /** * Gets the preview ImageView for use with image loading libraries. * * @return the preview ImageView */ public ImageView getPreviewImageView() { return previewImageView; } public void setControls(@Nullable VideoControls controls) { if (videoControls != null && videoControls != controls) { removeView(videoControls); } if (controls != null) { videoControls = controls; controls.setVideoView(this); addView(controls); } //Sets the onTouch listener to show the controls TouchListener listener = new TouchListener(getContext()); setOnTouchListener(videoControls != null ? listener : null); } /** * Requests the {@link VideoControls} to become visible. This should only be called after * {@link #setControls(VideoControls)}. */ public void showControls() { if (videoControls != null) { videoControls.show(); if (isPlaying()) { videoControls.hideDelayed(); } } } /** * Retrieves the video controls being used by this view. * If the controls haven't been specified with {@link #setControls(VideoControls)} * or through the XML attribute <code>useDefaultControls</code> this will return * null * * @return The video controls being used by this view or null */ @Nullable public VideoControls getVideoControls() { return videoControls; } /** * Sets the Uri location for the video to play * * @param uri The video's Uri */ public void setVideoURI(@Nullable Uri uri) { videoUri = uri; videoViewImpl.setVideoUri(uri); if (videoControls != null) { videoControls.showLoading(true); } } /** * Sets the Uri location for the video to play * * @param uri The video's Uri * @param mediaSource MediaSource that should be used */ public void setVideoURI(@Nullable Uri uri, @Nullable MediaSource mediaSource) { videoUri = uri; videoViewImpl.setVideoUri(uri, mediaSource); if (videoControls != null) { videoControls.showLoading(true); } } /** * Sets the path to the video. This path can be a web address (e.g. http://) or * an absolute local path (e.g. file://) * * @param path The path to the video */ public void setVideoPath(String path) { setVideoURI(Uri.parse(path)); } /** * Retrieves the current Video URI. If this hasn't been set with {@link #setVideoURI(android.net.Uri)} * or {@link #setVideoPath(String)} then null will be returned. * * @return The current video URI or null */ @Nullable public Uri getVideoUri() { return videoUri; } /** * Sets the {@link MediaDrmCallback} to use when handling DRM for media. * This should be called before specifying the videos uri or path * <br> * <b>NOTE:</b> DRM is only supported on API 18 + * * @param drmCallback The callback to use when handling DRM media */ public void setDrmCallback(@Nullable MediaDrmCallback drmCallback) { videoViewImpl.setDrmCallback(drmCallback); } /** * Sets the volume level for devices that support * the ExoPlayer (JellyBean or greater). * * @param volume The volume range [0.0 - 1.0] * @return True if the volume was set */ public boolean setVolume(@FloatRange(from = 0.0, to = 1.0) float volume) { return videoViewImpl.setVolume(volume); } /** * Enables or Disables automatic handling of audio focus. By default this is enabled * however in instances where a service handles playback of both audio and video it * is recommended to disable this and manually handle it in the service for consistency * * @param handleAudioFocus {@code true} to handle audio focus */ public void setHandleAudioFocus(boolean handleAudioFocus) { audioFocusHelper.abandonFocus(); this.handleAudioFocus = handleAudioFocus; } /** * Stops the current video playback and resets the listener states * so that we receive the callbacks for events like onPrepared */ public void reset() { stopPlayback(); setVideoURI(null); } /** * Moves the current video progress to the specified location. * * @param milliSeconds The time to move the playback to */ public void seekTo(long milliSeconds) { if (videoControls != null) { videoControls.showLoading(false); } videoViewImpl.seekTo(milliSeconds); } /** * Returns if a video is currently in playback * * @return True if a video is playing */ public boolean isPlaying() { return videoViewImpl.isPlaying(); } /** * Starts the playback for the video specified in {@link #setVideoURI(android.net.Uri)} * or {@link #setVideoPath(String)}. This should be called after the VideoView is correctly * prepared (see {@link #setOnPreparedListener(OnPreparedListener)}) */ public void start() { if (!audioFocusHelper.requestFocus()) { return; } videoViewImpl.start(); setKeepScreenOn(true); if (videoControls != null) { videoControls.updatePlaybackState(true); } } /** * If a video is currently in playback, it will be paused */ public void pause() { audioFocusHelper.abandonFocus(); videoViewImpl.pause(); setKeepScreenOn(false); if (videoControls != null) { videoControls.updatePlaybackState(false); } } /** * If a video is currently in playback then the playback will be stopped */ public void stopPlayback() { stopPlayback(true); } /** * If the video has completed playback, calling {@code restart} will seek to the beginning of the video, and play it. * * @return {@code true} if the video was successfully restarted, otherwise {@code false} */ public boolean restart() { if (videoUri == null) { return false; } if (videoViewImpl.restart()) { if (videoControls != null) { videoControls.showLoading(true); } return true; } else { return false; } } /** * If a video is currently in playback then the playback will be suspended */ public void suspend() { audioFocusHelper.abandonFocus(); videoViewImpl.suspend(); setKeepScreenOn(false); if (videoControls != null) { videoControls.updatePlaybackState(false); } } /** * Retrieves the duration of the current audio item. This should only be called after * the item is prepared (see {@link #setOnPreparedListener(OnPreparedListener)}). * If {@link #overrideDuration(long)} is set then that value will be returned. * * @return The millisecond duration of the video */ public long getDuration() { if (overriddenDuration >= 0) { return overriddenDuration; } return videoViewImpl.getDuration(); } /** * Setting this will override the duration that the item may actually be. This method should * only be used when the item doesn't return the correct duration such as with audio streams. * This only overrides the current audio item. * * @param duration The duration for the current media item or < 0 to disable */ public void overrideDuration(long duration) { overriddenDuration = duration; } /** * Retrieves the current position of the audio playback. If an audio item is not currently * in playback then the value will be 0. This should only be called after the item is * prepared (see {@link #setOnPreparedListener(OnPreparedListener)}) * * @return The millisecond value for the current position */ public long getCurrentPosition() { if (overridePosition) { return positionOffset + overriddenPositionStopWatch.getTimeInt(); } return positionOffset + videoViewImpl.getCurrentPosition(); } /** * Sets the amount of time to change the return value from {@link #getCurrentPosition()}. * This value will be reset when a new audio item is selected. * * @param offset The millisecond value to offset the position */ public void setPositionOffset(long offset) { positionOffset = offset; } /** * Restarts the audio position to the start if the position is being overridden (see {@link #overridePosition(boolean)}). * This will be the value specified with {@link #setPositionOffset(long)} or 0 if it hasn't been set. */ public void restartOverridePosition() { overriddenPositionStopWatch.reset(); } /** * Sets if the audio position should be overridden, allowing the time to be restarted at will. This * is useful for streaming audio where the audio doesn't have breaks between songs. * * @param override True if the position should be overridden */ public void overridePosition(boolean override) { if (override) { overriddenPositionStopWatch.start(); } else { overriddenPositionStopWatch.stop(); } overridePosition = override; } /** * Retrieves the current buffer percent of the video. If a video is not currently * prepared or buffering the value will be 0. This should only be called after the video is * prepared (see {@link #setOnPreparedListener(OnPreparedListener)}) * * @return The integer percent that is buffered [0, 100] inclusive */ public int getBufferPercentage() { return videoViewImpl.getBufferedPercent(); } /** * Sets the playback speed for this MediaPlayer. * * @param speed The speed to play the media back at * @return True if the speed was set */ public boolean setPlaybackSpeed(float speed) { return videoViewImpl.setPlaybackSpeed(speed); } /** * Determines if the current video player implementation supports * track selection for audio or video tracks. * * @return True if tracks can be manually specified */ public boolean trackSelectionAvailable() { return videoViewImpl.trackSelectionAvailable(); } /** * Changes to the track with <code>trackIndex</code> for the specified * <code>trackType</code> * * @param trackType The type for the track to switch to the selected index * @param trackIndex The index for the track to switch to */ public void setTrack(ExoMedia.RendererType trackType, int trackIndex) { videoViewImpl.setTrack(trackType, trackIndex); } /** * Retrieves a list of available tracks to select from. Typically {@link #trackSelectionAvailable()} * should be called before this. * * @return A list of available tracks associated with each track type */ @Nullable public Map<ExoMedia.RendererType, TrackGroupArray> getAvailableTracks() { return videoViewImpl.getAvailableTracks(); } /** * Sets how the video should be scaled in the view * * @param scaleType how to scale the videos */ public void setScaleType(@NonNull ScaleType scaleType) { videoViewImpl.setScaleType(scaleType); } /** * Measures the underlying {@link VideoViewApi} using the video's aspect ratio if {@code true} * * @param measureBasedOnAspectRatioEnabled whether to measure using the video's aspect ratio or not */ public void setMeasureBasedOnAspectRatioEnabled(boolean measureBasedOnAspectRatioEnabled) { videoViewImpl.setMeasureBasedOnAspectRatioEnabled(measureBasedOnAspectRatioEnabled); } /** * Sets the rotation for the Video * * @param rotation The rotation to apply to the video */ public void setVideoRotation(@IntRange(from = 0, to = 359) int rotation) { videoViewImpl.setVideoRotation(rotation, true); } /** * Sets the listener to inform of VideoPlayer prepared events * * @param listener The listener */ public void setOnPreparedListener(@Nullable OnPreparedListener listener) { listenerMux.setOnPreparedListener(listener); } /** * Sets the listener to inform of VideoPlayer completion events * * @param listener The listener */ public void setOnCompletionListener(@Nullable OnCompletionListener listener) { listenerMux.setOnCompletionListener(listener); } /** * Sets the listener to inform of VideoPlayer buffer update events * * @param listener The listener */ public void setOnBufferUpdateListener(@Nullable OnBufferUpdateListener listener) { listenerMux.setOnBufferUpdateListener(listener); } /** * Sets the listener to inform of VideoPlayer seek completion events * * @param listener The listener */ public void setOnSeekCompletionListener(@Nullable OnSeekCompletionListener listener) { listenerMux.setOnSeekCompletionListener(listener); } /** * Sets the listener to inform of playback errors * * @param listener The listener */ public void setOnErrorListener(@Nullable OnErrorListener listener) { listenerMux.setOnErrorListener(listener); } /** * Sets the listener to inform of ID3 metadata updates * * @param listener The listener */ public void setId3MetadataListener(@Nullable MetadataListener listener) { listenerMux.setMetadataListener(listener); } /** * Sets the listener to inform of video size changes * * @param listener The listener */ public void setOnVideoSizedChangedListener(@Nullable OnVideoSizeChangedListener listener) { muxNotifier.videoSizeChangedListener = listener; } /** * Performs the functionality to setup the initial properties including * determining the backing implementation and reading xml attributes * * @param context The context to use for setting up the view * @param attrs The xml attributes associated with this instance */ protected void setup(Context context, @Nullable AttributeSet attrs) { if (isInEditMode()) { return; } audioManager = (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE); AttributeContainer attributeContainer = new AttributeContainer(context, attrs); initView(context, attributeContainer); postInit(attributeContainer); } /** * Performs the initialization of the view including inflating the correct * backing layout, linking the implementation, and finding the necessary view * references. * * @param context The context for the initialization * @param attributeContainer The attributes associated with this instance */ protected void initView(Context context, @NonNull AttributeContainer attributeContainer) { inflateVideoView(context, attributeContainer); previewImageView = (ImageView) findViewById(R.id.exomedia_video_preview_image); videoViewImpl = (VideoViewApi) findViewById(R.id.exomedia_video_view); muxNotifier = new MuxNotifier(); listenerMux = new ListenerMux(muxNotifier); videoViewImpl.setListenerMux(listenerMux); } /** * Handles any setup that needs to be performed after {@link #initView(Context, AttributeContainer)} * is performed. * * @param attributeContainer The attributes associated with this instance */ protected void postInit(@NonNull AttributeContainer attributeContainer) { if (attributeContainer.useDefaultControls) { setControls(deviceUtil.isDeviceTV(getContext()) ? new VideoControlsLeanback(getContext()) : new VideoControlsMobile(getContext())); } if (attributeContainer.scaleType != null) { setScaleType(attributeContainer.scaleType); } if (attributeContainer.measureBasedOnAspectRatio != null) { setMeasureBasedOnAspectRatioEnabled(attributeContainer.measureBasedOnAspectRatio); } } /** * Inflates the video view layout, replacing the {@link ViewStub} with the * correct backing implementation. * * @param context The context to use for inflating the correct video view * @param attributeContainer The attributes for retrieving custom backing implementations. */ protected void inflateVideoView(@NonNull Context context, @NonNull AttributeContainer attributeContainer) { View.inflate(context, R.layout.exomedia_video_view_layout, this); ViewStub videoViewStub = (ViewStub) findViewById(R.id.video_view_api_impl_stub); videoViewStub.setLayoutResource(getVideoViewApiImplementation(context, attributeContainer)); videoViewStub.inflate(); } /** * Retrieves the layout resource to use for the backing video view implementation. By * default this uses the Android {@link android.widget.VideoView} on legacy devices with * APIs below Jellybean (16) or that don't pass the Compatibility Test Suite [CTS] via * {@link NativeTextureVideoView}, and an ExoPlayer backed video view on the remaining devices via * {@link ExoTextureVideoView}. * <p> * In the rare cases that the default implementations need to be extended, or replaced, the * user can override the value with the attributes <code>videoViewApiImplLegacy</code> * and <code>videoViewApiImpl</code>. * <p> * <b>NOTE:</b> overriding the default implementations may cause inconsistencies and isn't * recommended. * * @param context The Context to use when retrieving the backing video view implementation * @param attributeContainer The attributes to use for finding overridden video view implementations * @return The layout resource for the backing implementation on the current device */ @LayoutRes protected int getVideoViewApiImplementation(@NonNull Context context, @NonNull AttributeContainer attributeContainer) { boolean useLegacy = !deviceUtil.supportsExoPlayer(context); return useLegacy ? attributeContainer.apiImplLegacyResourceId : attributeContainer.apiImplResourceId; } /** * Performs the functionality to stop the progress polling, and stop any other * procedures from running that we no longer need. */ protected void onPlaybackEnded() { stopPlayback(false); } /** * Stops the video currently in playback, making sure to only clear the surface * when requested. This allows us to leave the last frame of a video intact when * it plays to completion while still clearing it when the user requests playback * to stop. * * @param clearSurface <code>true</code> if the surface should be cleared */ protected void stopPlayback(boolean clearSurface) { audioFocusHelper.abandonFocus(); videoViewImpl.stopPlayback(clearSurface); setKeepScreenOn(false); if (videoControls != null) { videoControls.updatePlaybackState(false); } } /** * A utility used to handle the audio focus for the {@link VideoView} * when enabled. */ protected class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener { protected boolean startRequested = false; protected boolean pausedForLoss = false; protected int currentFocus = 0; @Override public void onAudioFocusChange(int focusChange) { if (!handleAudioFocus || currentFocus == focusChange) { return; } currentFocus = focusChange; switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: if (startRequested || pausedForLoss) { start(); startRequested = false; pausedForLoss = false; } break; case AudioManager.AUDIOFOCUS_LOSS: if (isPlaying()) { pausedForLoss = true; pause(); } break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: if (isPlaying()) { pausedForLoss = true; pause(); } break; } } /** * Requests to obtain the audio focus * * @return True if the focus was granted */ public boolean requestFocus() { if (!handleAudioFocus || currentFocus == AudioManager.AUDIOFOCUS_GAIN) { return true; } if (audioManager == null) { return false; } int status = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (AudioManager.AUDIOFOCUS_REQUEST_GRANTED == status) { currentFocus = AudioManager.AUDIOFOCUS_GAIN; return true; } startRequested = true; return false; } /** * Requests the system to drop the audio focus * * @return True if the focus was lost */ public boolean abandonFocus() { if (!handleAudioFocus) { return true; } if (audioManager == null) { return false; } startRequested = false; int status = audioManager.abandonAudioFocus(this); return AudioManager.AUDIOFOCUS_REQUEST_GRANTED == status; } } protected class MuxNotifier extends ListenerMux.Notifier { @Nullable public OnVideoSizeChangedListener videoSizeChangedListener; @Override public boolean shouldNotifyCompletion(long endLeeway) { return getCurrentPosition() + endLeeway >= getDuration(); } @Override public void onExoPlayerError(ExoMediaPlayer exoMediaPlayer, Exception e) { stopPlayback(); if (exoMediaPlayer != null) { exoMediaPlayer.forcePrepare(); } } @Override public void onMediaPlaybackEnded() { setKeepScreenOn(false); onPlaybackEnded(); } @Override public void onSeekComplete() { if (videoControls != null) { videoControls.finishLoading(); } } @Override @SuppressWarnings("SuspiciousNameCombination") public void onVideoSizeChanged(int width, int height, int unAppliedRotationDegrees, float pixelWidthHeightRatio) { //NOTE: Android 5.0+ will always have an unAppliedRotationDegrees of 0 (ExoPlayer already handles it) videoViewImpl.setVideoRotation(unAppliedRotationDegrees, false); videoViewImpl.onVideoSizeChanged(width, height); if (videoSizeChangedListener != null) { videoSizeChangedListener.onVideoSizeChanged(width, height); } } @Override public void onPrepared() { if (videoControls != null) { videoControls.setDuration(getDuration()); videoControls.finishLoading(); } } @Override public void onPreviewImageStateChanged(boolean toVisible) { if (previewImageView != null) { previewImageView.setVisibility(toVisible ? View.VISIBLE : View.GONE); } } } /** * Monitors the view click events to show and hide the video controls if they have been specified. */ protected class TouchListener extends GestureDetector.SimpleOnGestureListener implements OnTouchListener { protected GestureDetector gestureDetector; public TouchListener(Context context) { gestureDetector = new GestureDetector(context, this); } @Override public boolean onTouch(View view, MotionEvent event) { gestureDetector.onTouchEvent(event); return true; } @Override public boolean onSingleTapConfirmed(MotionEvent event) { // Toggles between hiding and showing the controls if (videoControls != null && videoControls.isVisible()) { videoControls.hide(); } else { showControls(); } return true; } } /** * A simple class that will retrieve the attributes and provide a simplified * interaction than passing around the {@link AttributeSet} */ protected class AttributeContainer { /** * Specifies if the {@link VideoControls} should be added to the view. These * can be added through source code with {@link #setControls(VideoControls)} */ public boolean useDefaultControls = false; /** * Specifies if the {@link VideoViewApi} implementations should use the {@link android.view.TextureView} * implementations. If this is false then the implementations will be based on * the {@link android.view.SurfaceView} */ public boolean useTextureViewBacking = false; /** * The resource id that points to a custom implementation for the <code>ExoPlayer</code> * backed {@link VideoViewApi} */ public int apiImplResourceId = R.layout.exomedia_default_exo_texture_video_view; /** * The resource id that points to a custom implementation for the Android {@link android.media.MediaPlayer} * backed {@link VideoViewApi}. This will only be used on devices that do not support the * <code>ExoPlayer</code> (see {@link DeviceUtil#supportsExoPlayer(Context)} for details) */ public int apiImplLegacyResourceId = R.layout.exomedia_default_native_texture_video_view; /** * Specifies the scale that the {@link VideoView} should use. If this is <code>null</code> * then the default value from the {@link com.devbrackets.android.exomedia.core.video.scale.MatrixManager} * will be used. */ @Nullable public ScaleType scaleType; /** * Specifies if the {@link VideoView} should be measured based on the aspect ratio. Because * the default value is different between the {@link com.devbrackets.android.exomedia.core.video.ResizingSurfaceView} * and {@link com.devbrackets.android.exomedia.core.video.ResizingTextureView} this will be <code>null</code> * when not specified. */ @Nullable public Boolean measureBasedOnAspectRatio; /** * Reads the attributes associated with this view, setting any values found * * @param context The context to retrieve the styled attributes with * @param attrs The {@link AttributeSet} to retrieve the values from */ public AttributeContainer(@NonNull Context context, @Nullable AttributeSet attrs) { if (attrs == null) { return; } TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VideoView); if (typedArray == null) { return; } useDefaultControls = typedArray.getBoolean(R.styleable.VideoView_useDefaultControls, useDefaultControls); useTextureViewBacking = typedArray.getBoolean(R.styleable.VideoView_useTextureViewBacking, useTextureViewBacking); if (typedArray.hasValue(R.styleable.VideoView_videoScale)) { scaleType = ScaleType.fromOrdinal(typedArray.getInt(R.styleable.VideoView_videoScale, -1)); } if (typedArray.hasValue(R.styleable.VideoView_measureBasedOnAspectRatio)) { measureBasedOnAspectRatio = typedArray.getBoolean(R.styleable.VideoView_measureBasedOnAspectRatio, false); } //Resets the default implementations based on useTextureViewBacking apiImplResourceId = useTextureViewBacking ? R.layout.exomedia_default_exo_texture_video_view : R.layout.exomedia_default_exo_surface_video_view; apiImplLegacyResourceId = useTextureViewBacking ? R.layout.exomedia_default_native_texture_video_view : R.layout.exomedia_default_native_surface_video_view; apiImplResourceId = typedArray.getResourceId(R.styleable.VideoView_videoViewApiImpl, apiImplResourceId); apiImplLegacyResourceId = typedArray.getResourceId(R.styleable.VideoView_videoViewApiImplLegacy, apiImplLegacyResourceId); typedArray.recycle(); } } }