diff --git a/modules/video_output/opengl/converter.h b/modules/video_output/opengl/converter.h
index 7e11bb05a6b5ed630834af51a8f9eaf5d38d9b36..dc053aca5f6ba84d34ef1374a186bd1b1828a024 100644
--- a/modules/video_output/opengl/converter.h
+++ b/modules/video_output/opengl/converter.h
@@ -183,6 +183,26 @@ struct opengl_tex_converter_t
     void (*pf_prepare_shader)(const opengl_tex_converter_t *tc,
                               const GLsizei *tex_width, const GLsizei *tex_height,
                               float alpha);
+
+    /**
+     * Callback to retrieve the transform matrix to apply to texture coordinates
+     *
+     * This function pointer can be NULL. If it is set, it may return NULL.
+     *
+     * Otherwise, it must return a 4x4 matrix, as an array of 16 floats in
+     * column-major order.
+     *
+     * This transform matrix maps 2D homogeneous texture coordinates of the
+     * form (s, t, 0, 1) with s and t in the inclusive range [0, 1] to the
+     * texture coordinate that should be used to sample that location from the
+     * texture.
+     *
+     * The returned pointer is owned by the converter module, and must not be
+     * freed before the module is closed.
+     *
+     * \param tc OpenGL tex converter
+     */
+    const float *(*pf_get_transform_matrix)(const opengl_tex_converter_t *tc);
 };
 
 /**
diff --git a/modules/video_output/opengl/converter_android.c b/modules/video_output/opengl/converter_android.c
index 6fb0a4e6317dfd28367faa3487073b4decdebf0c..64680686407586a26810782940df06acb9a44329 100644
--- a/modules/video_output/opengl/converter_android.c
+++ b/modules/video_output/opengl/converter_android.c
@@ -35,10 +35,6 @@ struct priv
     AWindowHandler *awh;
     const float *transform_mtx;
     bool stex_attached;
-
-    struct {
-        GLint uSTMatrix;
-    } uloc;
 };
 
 static int
@@ -87,24 +83,11 @@ tc_anop_update(const opengl_tex_converter_t *tc, GLuint *textures,
     return VLC_SUCCESS;
 }
 
-static int
-tc_anop_fetch_locations(opengl_tex_converter_t *tc, GLuint program)
+static const float *
+tc_get_transform_matrix(const opengl_tex_converter_t *tc)
 {
     struct priv *priv = tc->priv;
-    priv->uloc.uSTMatrix = tc->vt->GetUniformLocation(program, "uSTMatrix");
-    return priv->uloc.uSTMatrix != -1 ? VLC_SUCCESS : VLC_EGENERIC;
-}
-
-static void
-tc_anop_prepare_shader(const opengl_tex_converter_t *tc,
-                       const GLsizei *tex_width, const GLsizei *tex_height,
-                       float alpha)
-{
-    (void) tex_width; (void) tex_height; (void) alpha;
-    struct priv *priv = tc->priv;
-    if (priv->transform_mtx != NULL)
-        tc->vt->UniformMatrix4fv(priv->uloc.uSTMatrix, 1, GL_FALSE,
-                                  priv->transform_mtx);
+    return priv->transform_mtx;
 }
 
 static void
@@ -147,13 +130,7 @@ Open(vlc_object_t *obj)
 
     tc->pf_allocate_textures = tc_anop_allocate_textures;
     tc->pf_update         = tc_anop_update;
-    tc->pf_fetch_locations = tc_anop_fetch_locations;
-    tc->pf_prepare_shader = tc_anop_prepare_shader;
-
-    tc->tex_count = 1;
-    tc->texs[0] = (struct opengl_tex_cfg) { { 1, 1 }, { 1, 1 }, 0, 0, 0 };
-
-    tc->tex_target   = GL_TEXTURE_EXTERNAL_OES;
+    tc->pf_get_transform_matrix = tc_get_transform_matrix;
 
     /* The transform Matrix (uSTMatrix) given by the SurfaceTexture is not
      * using the same origin than us. Ask the caller to rotate textures
@@ -186,29 +163,14 @@ Open(vlc_object_t *obj)
             break;
     }
 
-    static const char *template =
-        "#version %u\n"
-        "#extension GL_OES_EGL_image_external : require\n"
-        "%s" /* precision */
-        "varying vec2 TexCoord0;"
-        "uniform samplerExternalOES sTexture;"
-        "uniform mat4 uSTMatrix;"
-        "void main()"
-        "{ "
-        "  gl_FragColor = texture2D(sTexture, (uSTMatrix * vec4(TexCoord0, 1, 1)).xy).rgba;"
-        "}";
-
-    char *code;
-    if (asprintf(&code, template, tc->glsl_version, tc->glsl_precision_header) < 0)
+    tc->fshader = opengl_fragment_shader_init(tc, GL_TEXTURE_EXTERNAL_OES,
+                                              VLC_CODEC_RGB32,
+                                              COLOR_SPACE_UNDEF);
+    if (!tc->fshader)
     {
         free(tc->priv);
         return VLC_EGENERIC;
     }
-    GLuint fragment_shader = tc->vt->CreateShader(GL_FRAGMENT_SHADER);
-    tc->vt->ShaderSource(fragment_shader, 1, (const char **) &code, NULL);
-    tc->vt->CompileShader(fragment_shader);
-    tc->fshader = fragment_shader;
-    free(code);
 
     return VLC_SUCCESS;
 }
diff --git a/modules/video_output/opengl/fragment_shaders.c b/modules/video_output/opengl/fragment_shaders.c
index 9694b52d631dc96574383765a81558840ce2a87f..bd9978fda40fba5bd2a655992517b1796e86e9d6 100644
--- a/modules/video_output/opengl/fragment_shaders.c
+++ b/modules/video_output/opengl/fragment_shaders.c
@@ -524,6 +524,11 @@ opengl_fragment_shader_init_impl(opengl_tex_converter_t *tc, GLenum tex_target,
     const char *sampler, *lookup, *coord_name;
     switch (tex_target)
     {
+        case GL_TEXTURE_EXTERNAL_OES:
+            sampler = "samplerExternalOES";
+            lookup = "texture2D";
+            coord_name = "TexCoord";
+            break;
         case GL_TEXTURE_2D:
             sampler = "sampler2D";
             lookup  = "texture2D";
@@ -545,7 +550,12 @@ opengl_fragment_shader_init_impl(opengl_tex_converter_t *tc, GLenum tex_target,
 #define ADD(x) vlc_memstream_puts(&ms, x)
 #define ADDF(x, ...) vlc_memstream_printf(&ms, x, ##__VA_ARGS__)
 
-    ADDF("#version %u\n%s", tc->glsl_version, tc->glsl_precision_header);
+    ADDF("#version %u\n", tc->glsl_version);
+
+    if (tex_target == GL_TEXTURE_EXTERNAL_OES)
+        ADDF("#extension GL_OES_EGL_image_external : require\n");
+
+    ADDF("%s", tc->glsl_precision_header);
 
     for (unsigned i = 0; i < tc->tex_count; ++i)
         ADDF("uniform %s Texture%u;\n"
diff --git a/modules/video_output/opengl/gl_common.h b/modules/video_output/opengl/gl_common.h
index 11c109a7a7d1ecffd67d2422998b12c8ee53b6bf..fb14ae3b66f4def3ff8a2bea239d40519674f489 100644
--- a/modules/video_output/opengl/gl_common.h
+++ b/modules/video_output/opengl/gl_common.h
@@ -52,6 +52,9 @@
 #ifndef GL_TEXTURE_RECTANGLE
 # define GL_TEXTURE_RECTANGLE 0x84F5
 #endif
+#ifndef GL_TEXTURE_EXTERNAL_OES
+# define GL_TEXTURE_EXTERNAL_OES 0x8D65
+#endif
 
 #ifndef APIENTRY
 # define APIENTRY
diff --git a/modules/video_output/opengl/vout_helper.c b/modules/video_output/opengl/vout_helper.c
index 3ab3f2c02f8126040554bda80e9867b44544dfd0..c19d95f4f7f958c4df5ea26fd0c02d6b961296bf 100644
--- a/modules/video_output/opengl/vout_helper.c
+++ b/modules/video_output/opengl/vout_helper.c
@@ -105,6 +105,7 @@ struct prgm
     } var;
 
     struct { /* UniformLocation */
+        GLint TransformMatrix;
         GLint OrientationMatrix;
         GLint ProjectionMatrix;
         GLint ViewMatrix;
@@ -315,12 +316,13 @@ static GLuint BuildVertexShader(const opengl_tex_converter_t *tc,
         "attribute vec4 MultiTexCoord0;\n"
         "%s%s"
         "attribute vec3 VertexPosition;\n"
+        "uniform mat4 TransformMatrix;\n"
         "uniform mat4 OrientationMatrix;\n"
         "uniform mat4 ProjectionMatrix;\n"
         "uniform mat4 ZoomMatrix;\n"
         "uniform mat4 ViewMatrix;\n"
         "void main() {\n"
-        " TexCoord0 = vec4(OrientationMatrix * MultiTexCoord0).st;\n"
+        " TexCoord0 = vec4(OrientationMatrix * TransformMatrix * MultiTexCoord0).st;\n"
         "%s%s"
         " gl_Position = ProjectionMatrix * ZoomMatrix * ViewMatrix\n"
         "               * vec4(VertexPosition, 1.0);\n"
@@ -463,6 +465,7 @@ opengl_link_program(struct prgm *prgm)
 } while (0)
 #define GET_ULOC(x, str) GET_LOC(Uniform, prgm->uloc.x, str)
 #define GET_ALOC(x, str) GET_LOC(Attrib, prgm->aloc.x, str)
+    GET_ULOC(TransformMatrix, "TransformMatrix");
     GET_ULOC(OrientationMatrix, "OrientationMatrix");
     GET_ULOC(ProjectionMatrix, "ProjectionMatrix");
     GET_ULOC(ViewMatrix, "ViewMatrix");
@@ -533,6 +536,7 @@ opengl_init_program(vout_display_opengl_t *vgl, vlc_video_context *context,
     tc->vt = &vgl->vt;
     tc->b_dump_shaders = b_dump_shaders;
     tc->pf_fragment_shader_init = opengl_fragment_shader_init_impl;
+    tc->pf_get_transform_matrix = NULL;
     tc->glexts = glexts;
 #if defined(USE_OPENGL_ES2)
     tc->is_gles = true;
@@ -1471,6 +1475,13 @@ static void DrawWithShaders(vout_display_opengl_t *vgl, struct prgm *prgm)
     vgl->vt.EnableVertexAttribArray(prgm->aloc.VertexPosition);
     vgl->vt.VertexAttribPointer(prgm->aloc.VertexPosition, 3, GL_FLOAT, 0, 0, 0);
 
+    const GLfloat *tm = tc->pf_get_transform_matrix
+                      ? tc->pf_get_transform_matrix(tc) : NULL;
+    if (!tm)
+        tm = identity;
+
+    vgl->vt.UniformMatrix4fv(prgm->uloc.TransformMatrix, 1, GL_FALSE, tm);
+
     vgl->vt.UniformMatrix4fv(prgm->uloc.OrientationMatrix, 1, GL_FALSE,
                              prgm->var.OrientationMatrix);
     vgl->vt.UniformMatrix4fv(prgm->uloc.ProjectionMatrix, 1, GL_FALSE,
@@ -1650,6 +1661,9 @@ int vout_display_opengl_Display(vout_display_opengl_t *vgl,
         vgl->vt.VertexAttribPointer(prgm->aloc.VertexPosition, 2, GL_FLOAT,
                                     0, 0, 0);
 
+        vgl->vt.UniformMatrix4fv(prgm->uloc.TransformMatrix, 1, GL_FALSE,
+                                 identity);
+
         vgl->vt.UniformMatrix4fv(prgm->uloc.OrientationMatrix, 1, GL_FALSE,
                                  prgm->var.OrientationMatrix);
         vgl->vt.UniformMatrix4fv(prgm->uloc.ProjectionMatrix, 1, GL_FALSE,