50 votes

Changement d'orientation de la VideoView Android avec la vidéo mise en mémoire tampon

J'essaie de reproduire les fonctionnalités de la dernière application YouTube sur le marché Android. Lorsque l'on regarde une vidéo, il y a deux présentations distinctes, l'une en mode portrait qui fournit des informations supplémentaires, et l'autre en mode paysage qui fournit une vue plein écran de la vidéo.

YouTube app portrait layout
L'application YouTupe en mode portrait

YouTube app landscape layout
L'application YouTube en mode paysage

(Désolé pour le caractère aléatoire des photos, mais ce sont les premières photos que j'ai pu trouver de l'agencement actuel)

C'est assez facile à faire normalement - il suffit de spécifier une disposition alternative dans layout-land et tout ira bien. Ce que l'application YouTube fait vraiment bien (et que j'essaie de reproduire), c'est qu'en cas de changement d'orientation, la vidéo continue d'être lue et n'a pas besoin de recommencer la mise en mémoire tampon depuis le début.

J'ai compris que le fait de surcharger onConfigurationChange() et de définir de nouveaux LayoutParameters me permet de redimensionner la vidéo sans forcer un rebuffer - cependant la vidéo se redimensionne aléatoirement à des largeurs/hauteurs différentes lorsque l'on fait tourner l'écran plusieurs fois. J'ai essayé de faire toutes sortes d'appels invalidate() sur le VideoView, j'ai essayé d'appeler RequestLayout() sur le conteneur RelativeLayout parent et j'ai essayé autant de choses différentes que possible, mais je n'arrive pas à le faire fonctionner correctement. Tout conseil serait grandement apprécié !

Voici mon code :

@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 : J'ai découvert dans logcat une sortie intéressante qui apparaît lorsque ma vidéo est tournée et qui semble être le coupable - bien que je n'aie aucune idée de la façon de le corriger :

Sortie de Logcat lors d'un redimensionnement correct (occupe toute la fenêtre)

Remarquez le 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

Sortie du logcat lors d'un redimensionnement incorrect (occupe une petite partie du plein écran)

Remarquez le 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

Peut-être quelqu'un sait-il ce qu'est la "superposition" et pourquoi elle n'obtient pas la bonne valeur de hauteur ?

0 votes

Je sais que c'est une question plus ancienne, mais j'ai posté ma solution aussi pour que les gens puissent voir une approche mise à jour.

0 votes

Cette réponse fonctionne pour moi - une ligne de code, puisque j'ai déjà sous-classé VideoView stackoverflow.com/a/22101290/1458948

57voto

Mark37 Points 1263

EDIT : (juin 2016)

Cette réponse est très ancienne (je pense à Android 2.2/2.3) et n'est probablement pas aussi pertinente que les autres réponses ci-dessous ! Consultez-les d'abord à moins que vous ne développiez sur un Android ancien :)


J'ai pu circonscrire le problème à la fonction onMeasure de la classe VideoView. En créant une classe enfant et en surchargeant la fonction onMeasure, j'ai pu obtenir la fonctionnalité souhaitée.

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);
    }
}

Ensuite, dans mon activité, j'ai fait ce qui suit :

@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 ligne :

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

est essentiel pour que cela fonctionne. Si vous effectuez l'appel setDimensions sans ce type, la vidéo ne sera toujours pas redimensionnée.

La seule autre chose que vous devez faire est de vous assurer que vous appelez setDimensions() dans la méthode onCreate() également, sinon votre vidéo ne commencera pas à être mise en mémoire tampon car la vidéo ne sera pas configurée pour dessiner sur une surface de n'importe quelle taille.

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

Une dernière clé Si vous vous demandez pourquoi le VideoView ne se redimensionne pas lors de la rotation, vous devez vous assurer que les dimensions sur lesquelles vous vous redimensionnez sont soit exactement égale ou inférieure à la zone visible. J'ai eu un très gros problème où je réglais la largeur/hauteur du VideoView sur la taille totale de l'écran alors que j'avais toujours la barre de notification/titre à l'écran et que cela ne redimensionnait pas du tout le VideoView. Le simple fait de supprimer la barre de notification et la barre de titre a résolu le problème.

J'espère que cela aidera quelqu'un à l'avenir !

0 votes

Merci pour l'information ! J'ai essayé et cette classe m'aide beaucoup pour la rotation. Cependant, sur mon appareil, l'écran se corrompt lorsque l'on passe en mode portrait. Avez-vous une idée de la raison pour laquelle cela se produit ? J'ai commencé stackoverflow.com/questions/6524659/ dans l'espoir de le découvrir, mais jusqu'à présent, je n'ai aucune idée.

0 votes

Je l'ai utilisé, mais il ne lit pas la vidéo.

25voto

LoungeKatt Points 692

Voici un moyen très simple de réaliser ce que vous voulez avec un minimum de code :

AndroidManifest.xml :

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

Note : Modifiez selon les besoins de votre API, ceci couvre les API 10+, mais les API inférieures nécessitent de supprimer la partie "screenSize|screenLayout|uiMode" de cette ligne.

Dans la méthode "OnCreate", généralement sous "super.onCreate", ajoutez :

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

Puis, quelque part, généralement au bas de la page, ajouter :

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

Cette méthode permet de redimensionner la vidéo en plein écran lorsque l'orientation a changé, sans interrompre la lecture. Il suffit de remplacer la méthode de configuration.

0 votes

Sur certains appareils, il peut être nécessaire de masquer le contrôleur de médias afin d'empêcher le redimensionnement de la vidéo. Cette opération peut être réalisée à l'aide de l'option mediaController.hide() fonction.

23voto

BoD Points 3490

Tout d'abord, merci beaucoup pour votre réponse détaillée.

J'avais le même problème, la vidéo était la plupart du temps plus petite, ou plus grande, ou déformée à l'intérieur du VideoView après une rotation.

J'ai essayé votre solution, mais j'ai aussi essayé des choses au hasard, et par hasard j'ai remarqué que si mon VideoView est centré dans son parent, il fonctionne magiquement tout seul (pas de VideoView personnalisé nécessaire ou quoi que ce soit d'autre).

Pour être plus précis, avec cette mise en page, je reproduis le problème la plupart du temps :

<?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>

Avec cette seule mise en page, je n'ai jamais eu ce problème (de plus, la vidéo est centrée, ce qui devrait être le cas de toute façon ;) :

<?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>

Il fonctionne également avec wrap_content au lieu de match_parent (la vidéo occupe toujours tout l'espace), ce qui n'a pas beaucoup de sens pour moi.

Quoi qu'il en soit, je n'ai pas d'explication à cela - cela ressemble à un bogue VideoView pour moi.

11voto

Zapek Points 126

VideoView utilise ce que l'on appelle une superposition, c'est-à-dire une zone où la vidéo est rendue. Cette superposition se trouve derrière la fenêtre qui contient le VideoView. VideoView perce un trou dans sa fenêtre pour que la superposition soit visible. Il le maintient ensuite en synchronisation avec la disposition (par exemple, si vous déplacez ou redimensionnez le VideoView, la superposition doit également être déplacée et redimensionnée).

Il y a un bogue quelque part pendant la phase de mise en page qui fait que la superposition utilise la taille précédente définie par VideoView.

Pour y remédier, sous-classez VideoView et surchargez onLayout :

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

et la superposition aura la taille correcte à partir des dimensions de l'agencement du VideoView.

7voto

dcow Points 2921

Reproduire l'application YouTube

J'ai réussi à construire un exemple de projet qui n'exige pas android:configChanges="orientation" ou d'un VideoView . Le résultat est identique à la façon dont l'application YouTube gère la rotation pendant la lecture des vidéos. En d'autres termes, la vidéo n'est pas n'a pas besoin d'être mis en pause, remis en mémoire tampon ou rechargé, et ne saute ni ne laisse tomber aucune trame audio lorsque l'orientation de l'appareil change.

Méthode optimale

Cette méthode utilise un TextureView et son accompagnement SurfaceTexture en tant que puits pour le MediaPlayer de l'image vidéo en cours. Puisqu'une SurfaceTexture utilise un objet de texture GL (simplement référencé par un entier du contexte GL), il est possible de conserver une référence à une SurfaceTexture en cas de changement de configuration. Le TextureView lui-même est détruit et recréé pendant le changement de configuration (avec l'activité de soutien), et le TextureView nouvellement créé est simplement mis à jour avec la référence à la SurfaceTexture avant qu'il ne soit attaché.

J'ai créé un exemple complet de fonctionnement montrant comment et quand initialiser votre MediaPlayer et un éventuel MediaController, et je soulignerai ci-dessous les parties intéressantes pour cette question :

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();
    }

    // ...

}

Étant donné que la solution utilise un fragment conservé plutôt qu'une activité qui gère manuellement les changements de configuration, vous pouvez exploiter pleinement le système d'inflation des ressources spécifiques à la configuration, comme vous le feriez naturellement. Malheureusement, si votre sdk minimum est inférieur à l'API 16, vous devrez essentiellement rétroporter TextureView (ce que je n'ai pas fait).

Enfin, si cela vous intéresse, vérifier ma question initiale détaillant : mon approche originale, la pile média d'Android, pourquoi cela n'a pas fonctionné, et la solution alternative.

0 votes

Le fragment retenu créera-t-il une fuite de mémoire ?

0 votes

@GZ95 non. Nous jetons toutes les références à un contexte et réutilisons toutes les références que nous conservons.

0 votes

J'essaie de mettre en œuvre votre solution, mais je n'arrive pas à faire en sorte que la vidéo soit conservée lors de la rotation. La vue devient toujours noire et la vidéo recommence au début. Quelle est la cause de ce problème ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X