Cambio de orientación de Android VideoView con vídeo almacenado en búfer

Estoy intentando replicar esencialmente la funcionalidad de la última aplicación de YouTube en el mercado de Android. Al ver un video hay dos diseños separados, uno en el retrato que proporciona información adicional, y uno en el paisaje que ofrece una vista a pantalla completa del video.

Diseño de la imagen de la aplicación de YouTube
Aplicación YouTupe en modo retrato

Disposición del paisaje de la aplicación de YouTube
Aplicación de YouTube en modo horizontal

(Lo siento por la aleatoriedad de las fotos, pero fueron las primeras fotos que pude encontrar de la distribución real)

Esto es bastante fácil de hacer normalmente – sólo especificar un diseño alternativo en el diseño de la tierra y todo será bueno. Lo que la aplicación de YouTube hace realmente bien (y lo que estoy tratando de replicar) es que en el cambio de orientación, el video continúa jugando y no tiene que volver a almacenar el buffer desde el principio.

He descubierto que la sobrecarga onConfigurationChange () y la configuración de nuevos LayoutParameters me permitirá cambiar el tamaño del video sin forzar un rebuffer – sin embargo, el video se escalará aleatoriamente a diferentes anchos / alturas al girar la pantalla varias veces. He intentado hacer todo tipo de llamadas invalidate () en el VideoView, intenté llamar a RequestLayout () en el contenedor Parent RelativeLayout y simplemente probar tantas cosas diferentes como pueda, pero no puedo conseguirlo para que funcione correctamente. Cualquier consejo sería muy apreciado!

Aquí está mi código:

@Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { questionText.setVisibility(View.GONE); respond.setVisibility(View.GONE); questionVideo.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); } else { questionText.setVisibility(View.VISIBLE); respond.setVisibility(View.VISIBLE); Resources r = getResources(); int height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150.0f, r.getDisplayMetrics()); questionVideo.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, height)); } } 

EDIT: He descubierto en logcat alguna salida interesante que aparece cuando mi video se rota, que parece ser el culpable – aunque no tengo ni idea de cómo solucionarlo:

Salida de Logcat al redimensionar correctamente (ocupa toda la ventana)

Observe el h = 726

 12-13 15:37:35.468 1262 1270 I ActivityManager: Config changed: { scale=1.0 imsi=310/4 loc=en_US touch=3 keys=1/1/2 nav=1/1 orien=2 layout=34 uiMode=17 seq=210} 12-13 15:37:35.561 1262 1268 I TIOverlay: Position/X0/Y76/W480/H225 12-13 15:37:35.561 1262 1268 I TIOverlay: Adjusted Position/X1/Y0/W403/H225 12-13 15:37:35.561 1262 1268 I TIOverlay: Rotation/90 12-13 15:37:35.561 1262 1268 I Overlay : v4l2_overlay_set_position:: w=480 h=224 12-13 15:37:35.561 1262 1268 I Overlay : v4l2_overlay_set_position:: w=402 h=726 12-13 15:37:35.561 1262 1268 I Overlay : dumping driver state: 12-13 15:37:35.561 1262 1268 I Overlay : output pixfmt: 12-13 15:37:35.561 1262 1268 I Overlay : w: 432 12-13 15:37:35.561 1262 1268 I Overlay : h: 240 12-13 15:37:35.561 1262 1268 I Overlay : color: 7 12-13 15:37:35.561 1262 1268 I Overlay : UYVY 12-13 15:37:35.561 1262 1268 I Overlay : v4l2_overlay window: 12-13 15:37:35.561 1262 1268 I Overlay : window l: 1 12-13 15:37:35.561 1262 1268 I Overlay : window t: 0 12-13 15:37:35.561 1262 1268 I Overlay : window w: 402 12-13 15:37:35.561 1262 1268 I Overlay : window h: 726 

Salida de Logcat al redimensionar incorrectamente (ocupa una pequeña porción de pantalla completa)

Observe el h = 480

 12-13 15:43:00.085 1262 1270 I ActivityManager: Config changed: { scale=1.0 imsi=310/4 loc=en_US touch=3 keys=1/1/2 nav=1/1 orien=2 layout=34 uiMode=17 seq=216} 12-13 15:43:00.171 1262 1268 I TIOverlay: Position/X0/Y76/W480/H225 12-13 15:43:00.171 1262 1268 I TIOverlay: Adjusted Position/X138/Y0/W266/H225 12-13 15:43:00.171 1262 1268 I TIOverlay: Rotation/90 12-13 15:43:00.179 1262 1268 I Overlay : v4l2_overlay_set_position:: w=480 h=224 12-13 15:43:00.179 1262 1268 I Overlay : v4l2_overlay_set_position:: w=266 h=480 12-13 15:43:00.179 1262 1268 I Overlay : dumping driver state: 12-13 15:43:00.179 1262 1268 I Overlay : output pixfmt: 12-13 15:43:00.179 1262 1268 I Overlay : w: 432 12-13 15:43:00.179 1262 1268 I Overlay : h: 240 12-13 15:43:00.179 1262 1268 I Overlay : color: 7 12-13 15:43:00.179 1262 1268 I Overlay : UYVY 12-13 15:43:00.179 1262 1268 I Overlay : v4l2_overlay window: 12-13 15:43:00.179 1262 1268 I Overlay : window l: 138 12-13 15:43:00.179 1262 1268 I Overlay : window t: 0 12-13 15:43:00.179 1262 1268 I Overlay : window w: 266 12-13 15:43:00.179 1262 1268 I Overlay : window h: 480 

Tal vez alguien sabe lo que 'superposición' es y por qué no está recibiendo el valor de altura correcta?

Fui capaz de reducir el problema a la función onMeasure en la clase VideoView. Mediante la creación de una clase de hijo y sobreescribir la función onMeasure, pude obtener la funcionalidad deseada.

 public class VideoViewCustom extends VideoView { private int mForceHeight = 0; private int mForceWidth = 0; public VideoViewCustom(Context context) { super(context); } public VideoViewCustom(Context context, AttributeSet attrs) { this(context, attrs, 0); } public VideoViewCustom(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public void setDimensions(int w, int h) { this.mForceHeight = h; this.mForceWidth = w; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.i("@@@@", "onMeasure"); setMeasuredDimension(mForceWidth, mForceHeight); } } 

Luego dentro de mi Actividad, hice lo siguiente:

 @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); questionVideo.setDimensions(displayHeight, displayWidth); questionVideo.getHolder().setFixedSize(displayHeight, displayWidth); } else { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); questionVideo.setDimensions(displayWidth, smallHeight); questionVideo.getHolder().setFixedSize(displayWidth, smallHeight); } } 

La línea:

 questionVideo.getHolder().setFixedSize(displayWidth, smallHeight); 

Es clave para que esto funcione. Si haces la llamada a setDimensions sin este tipo, el video todavía no cambiará de tamaño.

La única otra cosa que debe hacer es asegurarse de que llama a setDimensions () dentro del método onCreate () o bien su video no iniciará el almacenamiento en búfer ya que el video no se configurará para dibujar en una superficie de cualquier tamaño.

 // onCreate() questionVideo.setDimensions(initialWidth, initialHeight); 

Una última parte clave – si alguna vez te encuentras preguntándose por qué el VideoView no está cambiando el tamaño de la rotación, debes asegurarte de que las dimensiones a las que cambias el tamaño sean exactamente iguales a las del área visible o menores. Tenía un problema realmente grande donde estaba fijando la anchura / altura del VideoView al tamaño de pantalla entero cuando todavía tenía la barra de la barra de la notificación / del título en la pantalla y no estaba cambiando de tamaño el VideoView en absoluto. Simplemente eliminando la barra de notificación y barra de título solucionado el problema.

Espero que esto ayude a alguien en el futuro!

EDIT: (junio de 2016)

Esta respuesta es muy antigua (creo que Android 2.2 / 2.3) y probablemente no es tan relevante como las otras respuestas a continuación! Mira a ellos primero a menos que estés dev-ing en el legado de Android 🙂

En primer lugar, muchas gracias por su extensa respuesta.

Tuve el mismo problema, el video sería la mayor parte del tiempo menor, o más grande, o distorsionado dentro de la VideoView después de una rotación.

He intentado su solución, pero también intenté cosas al azar, y por casualidad me di cuenta de que si mi VideoView se centra en su padre, funciona mágicamente por sí mismo (no VideoView personalizado o nada).

Para ser más específico, con este diseño, reproduzco el problema la mayor parte del tiempo:

 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <VideoView android:id="@+id/videoView" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout> 

Con este diseño, nunca tengo el problema (además, el video está centrado, que es cómo debe ser de todos modos;):

 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <VideoView android:id="@+id/videoView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" /> </RelativeLayout> 

También funciona con wrap_content lugar de match_parent (el video todavía toma todo el espacio) que no tiene mucho sentido para mí.

De todos modos, no tengo ninguna explicación para esto – esto parece un error de VideoView para mí.

Esta es una manera muy fácil de lograr lo que quieres con un código mínimo:

AndroidManifest.xml:

 android:configChanges="orientation|keyboard|keyboardHidden|screenSize|screenLayout|uiMode" 

Nota: Edite según sea necesario para su API, esto cubre 10+, pero las API más bajas requieren la eliminación de la parte "screenSize | screenLayout | uiMode" de esta línea

Dentro del método "OnCreate", generalmente bajo "super.onCreate", agregue:

 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); 

Y luego en algún lugar, normalmente en la parte inferior, agregue:

 @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } 

Esto cambiará el tamaño del vídeo a pantalla completa siempre que la orientación haya cambiado sin interrumpir la reproducción y solo se necesitará sobreescribir el método de configuración.

VideoView utiliza lo que se llama una superposición que es un área donde se procesa el vídeo. Esta superposición está detrás de la ventana que sostiene el VideoView. VideoView perfora un agujero en su ventana para que la superposición sea visible. A continuación, lo mantiene en sincronía con el diseño (por ejemplo, si mueve o cambia de tamaño VideoView, la superposición tiene que moverse y cambiar el tamaño también).

Hay un error en algún lugar durante la fase de diseño que hace que la superposición utilizar el tamaño anterior establecido por VideoView.

Para corregirlo, subclase VideoView y anula onLayout:

 @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); getHolder().setSizeFromLayout(); } 

Y la superposición tendrá el tamaño correcto de las dimensiones de diseño de VideoView.

Replicar la aplicación de YouTube

Me las arreglé para construir un proyecto de ejemplo que no requiere android:configChanges="orientation" o un VideoView personalizado. La experiencia resultante es idéntica a cómo maneja la aplicación de YouTube la rotación durante la reproducción de vídeo. En otras palabras, el video no necesita ser pausado, re-búfer o recargado, y no salta o deja caer ninguna trama de audio cuando cambia la orientación del dispositivo.

Método óptimo

Este método utiliza un TextureView y su SurfaceTexture como un sumidero para el marco de vídeo actual del MediaPlayer . Dado que SurfaceTexture utiliza un objeto de textura GL (simplemente referenciado por un entero del contexto GL), cree que está bien conservar una referencia a SurfaceTexture a través de cambios de configuración. El TextureView mismo se destruye y se vuelve a crear durante el cambio de configuración (junto con la actividad de respaldo) y el TextureView recién creado se actualiza simplemente con la referencia SurfaceTexture antes de adjuntarla.

He creado un ejemplo completo de trabajo que muestra cómo y cuándo inicializar su MediaPlayer y un posible MediaController, y voy a destacar las partes interesantes pertinentes a esta pregunta a continuación:

 public class VideoFragment { TextureView mDisplay; SurfaceTexture mTexture; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View rootView = inflater.inflate(R.layout.fragment_main, container, false); mDisplay = (TextureView) rootView.findViewById(R.id.texture_view); if (mTexture != null) { mDisplay.setSurfaceTexture(mTexture); } mDisplay.setSurfaceTextureListener(mTextureListener); return rootView; } TextureView.SurfaceTextureListener mTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mTexture = surface; // Initialize your media now or flag that the SurfaceTexture is available.. } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { mTexture = surface; } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { mTexture = surface; return false; // this says you are still using the SurfaceTexture.. } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { mTexture = surface; } }; @Override public void onDestroyView() { mDisplay = null; super.onDestroyView(); } // ... } 

Puesto que la solución utiliza un Fragmento retenido en lugar de una Actividad que maneja los cambios de configuración manualmente, puede aprovechar completamente el sistema de inflación de recursos específicos de la configuración como lo haría naturalmente. Por desgracia, si su sdk mínimo está por debajo de API 16, esencialmente tendrá que backport TextureView (que no he hecho).

Por último, si está interesado, revise mi pregunta inicial detallando: mi enfoque original, la pila de medios de Android, por qué no funcionó y la solución alternativa.

utilizar esta :

 @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { getActivity().getWindow().addFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN); getActivity().getWindow().clearFlags( WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { getActivity().getWindow().addFlags( WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); getActivity().getWindow().clearFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN); } } 

También no se olvide de añadir la línea a continuación a su actividad en el manifiesto:

 android:configChanges="orientation|keyboard|keyboardHidden|screenSize" 

Tomé ejemplos de algunas de las respuestas aquí e intenté hacerlo a mi manera. Parece una solución fácil.

 videoLayout = (RelativeLayout) videoView.findViewById(R.id.videoFrame); 

OnConfigurationChange dentro de un fragmento. Cuando el video se muestra en pantalla completa en modo horizontal. Tenga en cuenta que estoy ocultando la barra de acción.

 @Override public void onConfigurationChanged(Configuration newConfig) { int height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f,getResources().getDisplayMetrics()); ActionBar actionBar = weatherActivity.getSupportActionBar(); LayoutParams params = videoLayout.getLayoutParams(); if(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { if(actionBar.isShowing()) actionBar.hide(); params.width = ViewGroup.LayoutParams.MATCH_PARENT; params.height = ViewGroup.LayoutParams.MATCH_PARENT; videoLayout.requestLayout(); } else if(newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { if(!actionBar.isShowing()) actionBar.show(); params.width = ViewGroup.LayoutParams.MATCH_PARENT; params.height = height; videoLayout.requestLayout(); } super.onConfigurationChanged(newConfig); } 

Y aquí está mi archivo de diseño

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <RelativeLayout android:id="@+id/videoFrame" android:layout_height="200dp" android:layout_width="match_parent"> <VideoView android:id="@+id/video" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerHorizontal="true"/> </RelativeLayout> 

Tengo otro relativelayout debajo de esto que no está en esta disposición. Pero eso no haría ninguna diferencia.

Ver mi código de ejemplo, funciona para mí

 public class CustomVideoView extends android.widget.VideoView { private int width; private int height; private Context context; private VideoSizeChangeListener listener; private boolean isFullscreen; public CustomVideoView(Context context) { super(context); init(context); } public CustomVideoView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } /** * get video screen width and height for calculate size * * @param context Context */ private void init(Context context) { this.context = context; setScreenSize(); } /** * calculate real screen size */ private void setScreenSize() { Display display = ((Activity) context).getWindowManager().getDefaultDisplay(); if (Build.VERSION.SDK_INT >= 17) { //new pleasant way to get real metrics DisplayMetrics realMetrics = new DisplayMetrics(); display.getRealMetrics(realMetrics); width = realMetrics.widthPixels; height = realMetrics.heightPixels; } else if (Build.VERSION.SDK_INT >= 14) { //reflection for this weird in-between time try { Method mGetRawH = Display.class.getMethod("getRawHeight"); Method mGetRawW = Display.class.getMethod("getRawWidth"); width = (Integer) mGetRawW.invoke(display); height = (Integer) mGetRawH.invoke(display); } catch (Exception e) { //this may not be 100% accurate, but it's all we've got width = display.getWidth(); height = display.getHeight(); } } else { //This should be close, as lower API devices should not have window navigation bars width = display.getWidth(); height = display.getHeight(); } // when landscape w > h, swap it if (width > height) { int temp = width; width = height; height = temp; } } /** * set video size change listener * */ public void setVideoSizeChangeListener(VideoSizeChangeListener listener) { this.listener = listener; } public interface VideoSizeChangeListener { /** * when landscape */ void onFullScreen(); /** * when portrait */ void onNormalSize(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { // full screen when landscape setSize(height, width); if (listener != null) listener.onFullScreen(); isFullscreen = true; } else { // height = width * 9/16 setSize(width, width * 9 / 16); if (listener != null) listener.onNormalSize(); isFullscreen = false; } } /** * @return true: fullscreen */ public boolean isFullscreen() { return isFullscreen; } /** * set video sie * * @param w Width * @param h Height */ private void setSize(int w, int h) { setMeasuredDimension(w, h); getHolder().setFixedSize(w, h); } } 

Y no vuelva a crear la vista cuando cambie la orientación

 // AndroidManifest.xml android:configChanges="screenSize|orientation|keyboardHidden" 

Portrain

paisaje paisaje

Mi código fuente aquí

Mientras que la respuesta de Mark37 (muy útil) funciona, requiere establecer las dimensiones manualmente (usando setDimensions). Esto puede estar bien en una aplicación donde las dimensiones deseadas se conocen de antemano, pero si desea que las vistas determinen el tamaño automáticamente basándose en los parámetros del vídeo (por ejemplo, para asegurarse de que se mantenga la relación de aspecto original) .

Afortunadamente, resulta que la parte setDimensions no es realmente necesaria. OnMeasure de VideoView ya incluye toda la lógica necesaria, así que en lugar de depender de alguien para llamar a setDimensions, es posible llamar a super.onMeasure y luego usar getMeasuredWidth / getMeasuredHeight para obtener el tamaño fijo.

Por lo tanto, la clase VideoViewCustom se convierte simplemente en:

 public class VideoViewCustom extends VideoView { public VideoViewCustom(Context context) { super(context); } public VideoViewCustom(Context context, AttributeSet attrs) { this(context, attrs, 0); } public VideoViewCustom(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); getHolder().setFixedSize(getMeasuredWidth(), getMeasuredHeight()); } } 

Esta versión no requiere ningún código adicional en la persona que llama y aprovecha al máximo la implementación de onMeasure existente de VideoView.

Esto es de hecho bastante similar al enfoque sugerido por Zapek, que hace básicamente lo mismo, sólo con onLayout en lugar de onMeasure.

Intenté usar el código de otras respuestas, pero no resolvió mi problema, porque si giré la pantalla rápidamente, mi tamaño de video fue superado. Así que utilizo en parte su código, que pongo en un hilo que actualiza la ventana muy rápidamente. Así que, aquí está mi código:

  new Thread(new Runnable() { @Override public void run() { while(true) { try { Thread.sleep(1); } catch(InterruptedException e){} handler.post(new Runnable() { @Override public void run() { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } }); } } }).start(); 

De esta manera, el tamaño del video siempre será correcto (en mi caso).

  • ¿Cómo uso realmente ffmpeg en Android?
  • Stream URL RTSP utilizando VideoView en android
  • No se puede reproducir vídeo RTSP en VideoView en Samsung Galaxy S2
  • Android VideoView cómo escalar como tipo de escala ImageView fitXY?
  • Cómo activar FullScreen con VideoView Android
  • Org.json.JSONException: Fin de entrada en el carácter 0 de
  • VideoView se convierte en pantalla negra
  • Android: videoview in dialogfragment
  • Android Media Server murió después de que alguna parte jugada con Error 100 0
  • Tableta Android con vídeo incorporado pierde video en rotación
  • Reproducir video mediante videoview en Listview
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.