API de iframe de YouTube: ¿cómo controlo un reproductor de iframe que ya está en HTML?

149

Quiero poder controlar jugadores de YouTube basados ​​en iframe. Estos reproductores ya estarán en HTML, pero quiero controlarlos a través de la API de JavaScript.

He estado leyendo la documentación de la API de iframe que explica cómo agregar un nuevo video a la página con la API, y luego controlarlo con las funciones del reproductor de YouTube:

var player;
function onYouTubePlayerAPIReady() {
    player = new YT.Player('container', {
        height: '390',
        width: '640',
        videoId: 'u1zgFlCw8Aw',
        events: {
            'onReady': onPlayerReady,
            'onStateChange': onPlayerStateChange
        }
    });
}

Ese código crea un nuevo objeto jugador y lo asigna a 'jugador', luego lo inserta dentro del #container div. Entonces puedo operar en 'jugador' y llamada playVideo(), pauseVideo()etc. en ella.

Pero quiero poder operar con reproductores de iframe que ya están en la página.

Podría hacer esto muy fácilmente con el antiguo método de inserción, con algo como:

player = getElementById('whateverID');
player.playVideo();

Pero esto no funciona con los nuevos iframes. ¿Cómo puedo asignar un objeto iframe que ya está en la página y luego usar las funciones API en él?

agente_secreto
fuente
He escrito una abstracción para trabajar con la API IFrame de YouTube github.com/gajus/playtube
Gajus

Respuestas:

316

Fiddle Links: Código fuente - Vista previa - Versión pequeña
Actualización: Esta pequeña función solo ejecutará código en una sola dirección. Si desea soporte completo (por ejemplo, oyentes / captadores de eventos), eche un vistazo a Escuchar eventos de Youtube en jQuery

Como resultado de un análisis de código profundo, he creado una función: function callPlayersolicita una llamada de función en cualquier video de YouTube enmarcado. Ver la referencia de la API de YouTube para obtener una lista completa de posibles llamadas a funciones. Lea los comentarios en el código fuente para obtener una explicación.

El 17 de mayo de 2012, el tamaño del código se duplicó para cuidar el estado de preparación del jugador. Si necesita una función compacta que no se ocupe del estado de preparación del reproductor, consulte http://jsfiddle.net/8R5y6/ .

/**
 * @author       Rob W <[email protected]>
 * @website      https://stackoverflow.com/a/7513356/938089
 * @version      20190409
 * @description  Executes function on a framed YouTube video (see website link)
 *               For a full list of possible functions, see:
 *               https://developers.google.com/youtube/js_api_reference
 * @param String frame_id The id of (the div containing) the frame
 * @param String func     Desired function to call, eg. "playVideo"
 *        (Function)      Function to call when the player is ready.
 * @param Array  args     (optional) List of arguments to pass to function func*/
function callPlayer(frame_id, func, args) {
    if (window.jQuery && frame_id instanceof jQuery) frame_id = frame_id.get(0).id;
    var iframe = document.getElementById(frame_id);
    if (iframe && iframe.tagName.toUpperCase() != 'IFRAME') {
        iframe = iframe.getElementsByTagName('iframe')[0];
    }

    // When the player is not ready yet, add the event to a queue
    // Each frame_id is associated with an own queue.
    // Each queue has three possible states:
    //  undefined = uninitialised / array = queue / .ready=true = ready
    if (!callPlayer.queue) callPlayer.queue = {};
    var queue = callPlayer.queue[frame_id],
        domReady = document.readyState == 'complete';

    if (domReady && !iframe) {
        // DOM is ready and iframe does not exist. Log a message
        window.console && console.log('callPlayer: Frame not found; id=' + frame_id);
        if (queue) clearInterval(queue.poller);
    } else if (func === 'listening') {
        // Sending the "listener" message to the frame, to request status updates
        if (iframe && iframe.contentWindow) {
            func = '{"event":"listening","id":' + JSON.stringify(''+frame_id) + '}';
            iframe.contentWindow.postMessage(func, '*');
        }
    } else if ((!queue || !queue.ready) && (
               !domReady ||
               iframe && !iframe.contentWindow ||
               typeof func === 'function')) {
        if (!queue) queue = callPlayer.queue[frame_id] = [];
        queue.push([func, args]);
        if (!('poller' in queue)) {
            // keep polling until the document and frame is ready
            queue.poller = setInterval(function() {
                callPlayer(frame_id, 'listening');
            }, 250);
            // Add a global "message" event listener, to catch status updates:
            messageEvent(1, function runOnceReady(e) {
                if (!iframe) {
                    iframe = document.getElementById(frame_id);
                    if (!iframe) return;
                    if (iframe.tagName.toUpperCase() != 'IFRAME') {
                        iframe = iframe.getElementsByTagName('iframe')[0];
                        if (!iframe) return;
                    }
                }
                if (e.source === iframe.contentWindow) {
                    // Assume that the player is ready if we receive a
                    // message from the iframe
                    clearInterval(queue.poller);
                    queue.ready = true;
                    messageEvent(0, runOnceReady);
                    // .. and release the queue:
                    while (tmp = queue.shift()) {
                        callPlayer(frame_id, tmp[0], tmp[1]);
                    }
                }
            }, false);
        }
    } else if (iframe && iframe.contentWindow) {
        // When a function is supplied, just call it (like "onYouTubePlayerReady")
        if (func.call) return func();
        // Frame exists, send message
        iframe.contentWindow.postMessage(JSON.stringify({
            "event": "command",
            "func": func,
            "args": args || [],
            "id": frame_id
        }), "*");
    }
    /* IE8 does not support addEventListener... */
    function messageEvent(add, listener) {
        var w3 = add ? window.addEventListener : window.removeEventListener;
        w3 ?
            w3('message', listener, !1)
        :
            (add ? window.attachEvent : window.detachEvent)('onmessage', listener);
    }
}

Uso:

callPlayer("whateverID", function() {
    // This function runs once the player is ready ("onYouTubePlayerReady")
    callPlayer("whateverID", "playVideo");
});
// When the player is not ready yet, the function will be queued.
// When the iframe cannot be found, a message is logged in the console.
callPlayer("whateverID", "playVideo");

Posibles preguntas (y respuestas):

P : ¡No funciona!
R : "No funciona" no es una descripción clara. ¿Recibe usted algún mensaje de error? Por favor, muestre el código relevante.

P : playVideono reproduce el video.
R : La reproducción requiere la interacción del usuario y la presencia de allow="autoplay"iframe. Ver https://developers.google.com/web/updates/2017/09/autoplay-policy-changes y https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide

P : ¡He incrustado un video de YouTube usando <iframe src="http://www.youtube.com/embed/As2rZGPGKDY" />pero la función no ejecuta ninguna función!
R : Debe agregar ?enablejsapi=1al final de su URL:/embed/vid_id?enablejsapi=1 .

P : Recibo el mensaje de error "Se especificó una cadena no válida o ilegal". ¿Por qué?
R : La API no funciona correctamente en un host local ( file://). Aloje su página (prueba) en línea o use JSFiddle . Ejemplos: Vea los enlaces en la parte superior de esta respuesta.

P : ¿Cómo supiste esto?
R : He pasado algún tiempo para interpretar manualmente la fuente de la API. Llegué a la conclusión de que tenía que usar el postMessagemétodo. Para saber qué argumentos pasar, creé una extensión de Chrome que intercepta los mensajes. El código fuente de la extensión se puede descargar aquí. .

P : ¿Qué navegadores son compatibles?
R : Todos los navegadores que admiten JSON y postMessage.

  • IE 8+
  • Firefox 3.6+ (en realidad 3.5, pero document.readyStatese implementó en 3.6)
  • Opera 10.50+
  • Safari 4+
  • Chrome 3+

Respuesta / implementación relacionada: Fade-in en un video enmarcado usando jQuery
Soporte completo de API: Escuchando el evento de Youtube en jQuery
API oficial: https://developers.google.com/youtube/iframe_api_reference

Revisión histórica

  • 17 mayo 2012
    Implementado onYouTubePlayerReady: callPlayer('frame_id', function() { ... }).
    Las funciones se ponen en cola automáticamente cuando el reproductor aún no está listo.
  • 24 de julio de 2012
    Actualizado y probado sucesivamente en los navegadores compatibles (mirar hacia adelante).
  • 10 de octubre de 2013 Cuando una función se pasa como argumento, callPlayerobliga a una verificación de disponibilidad. Esto es necesario, porque cuando callPlayerse llama justo después de la inserción del iframe mientras el documento está listo, no puede saber con certeza si el iframe está completamente listo. En Internet Explorer y Firefox, este escenario resultó en una invocación demasiado temprana de postMessage, lo cual fue ignorado.
  • 12 de diciembre de 2013, se recomienda agregar &origin=*en la URL.
  • 2 de marzo de 2014, recomendación retirada para eliminar &origin=*a la URL.
  • 9 de abril de 2019, corrija el error que resultó en una recursión infinita cuando YouTube se carga antes de que la página esté lista. Añadir nota sobre reproducción automática.
Rob W
fuente
@RobW Lo intenté en realidad. Parece que el JSON por error no es el que está en su script, sino dentro del iframe como parte de la API de youtube.
Fresheyeball
@RobW gracias por este bonito fragmento. ¿Ha encontrado alguna manera de usar el evento de mensaje en lugar de usar la API JS de youtube para agregar un detector de eventos?
brillante
@ brillout.com El PostMessagemétodo se basa en la API YT JS ( ?enablejsapi=1). Sin habilitar la API JS, el postMessagemétodo no hará nada. Consulte la respuesta vinculada para una implementación sencilla de los oyentes de eventos. También he creado, pero no publicado, código legible para comunicarme con el marco. Decidí no publicarlo, porque su efecto es similar al API Frame predeterminado de YouTube.
Rob W
1
@MatthewBaker Eso requiere escuchar el evento del mensaje y analizar el estado del resultado. Esto no es tan fácil como las llamadas simples playVideo, así que recomiendo usar la API oficial para eso. Consulta developers.google.com/youtube/iframe_api_reference#Events .
Rob W
1
@ffyeahh No veo ningún error obvio. Haga una nueva pregunta con pasos independientes para reproducir en lugar de agregar preguntas en los comentarios a esta respuesta.
Rob W
33

¡Parece que YouTube ha actualizado su API JS, por lo que está disponible de forma predeterminada! Puede usar una ID de iframe de YouTube existente ...

<iframe id="player" src="http://www.youtube.com/embed/M7lc1UVf-VE?enablejsapi=1&origin=http://example.com" frameborder="0"></iframe>

... en tu JS ...

var player;
function onYouTubeIframeAPIReady() {
  player = new YT.Player('player', {
    events: {
      'onStateChange': onPlayerStateChange
    }
  });
}

function onPlayerStateChange() {
  //...
}

... y el constructor usará su iframe existente en lugar de reemplazarlo por uno nuevo. Esto también significa que no tiene que especificar el videoId al constructor.

Ver Cargando un reproductor de video

CletusW
fuente
1
@raven te falta el parámetro autoplay = 1 en la url. En su ejemplo de URL, sería youtube.com/embed/M7lc1UVf-VE?enablejsapi=1& autoplay = 1 & origin = example.com
alengel
@alengel no quiere usar el parámetro autoplay-url. En su lugar, intenta iniciar el video utilizando la función de reproducción automática js-APIs. Pero por alguna razón, no se invoca la función onYouTubeIframeAPIReady ().
Humppakäräjät
@raven lo descubrí. 1) elimine el & origin = example.com de la url del iframe. 2) en la sección "Frameworks & Extensions" de su jsfiddle, configure el segundo menú desplegable en "No wrap in - <head>" 3) agregue la API iframe de youtube como recurso externo ( youtube.com/iframe_api ); Bifurqué tu violín y apliqué estos cambios: jsfiddle.net/e97famd1/1
Humppakäräjät
¿Alguna idea de qué evento commandenviar al iframe de YT para detener listeningel estado?
mkhatib
@CletusW: me sale este error: DOMException no capturado (en promesa): la solicitud play () fue interrumpida por una llamada a pausa (). Promise (async) (anónimo) @ scripts.js: 20 dispatch @ jquery-1.12.4.js: 5226 elemData.handle @ jquery-1.12.4.js: 4878 cast_sender.js: 67 DOMException no capturado: no se pudo construir 'PresentationRequest ': Presentación de un documento inseguro [cast: 233637DE? Capacidades = video_out% 2Caudio_out & clientId = 153262711713390989 & autoJoinPolicy = tab_and_origin_scoped & defaultActionPolicy = cast_this_tab & launchTimeout = 30000] está prohibido desde un contexto seguro.
LauraNMS
20

Puede hacer esto con mucho menos código:

function callPlayer(func, args) {
    var i = 0,
        iframes = document.getElementsByTagName('iframe'),
        src = '';
    for (i = 0; i < iframes.length; i += 1) {
        src = iframes[i].getAttribute('src');
        if (src && src.indexOf('youtube.com/embed') !== -1) {
            iframes[i].contentWindow.postMessage(JSON.stringify({
                'event': 'command',
                'func': func,
                'args': args || []
            }), '*');
        }
    }
}

Ejemplo de trabajo: http://jsfiddle.net/kmturley/g6P5H/296/

Kim T
fuente
Realmente me gustó esta forma, simplemente lo adapté para trabajar con una directiva angular, por lo que no necesité todo el bucle y pasé el func dependiendo de una función de alternar con el alcance (básicamente si se muestra el video -> reproducción automática; de lo contrario -> pausa el video). ¡Gracias!
DD.
Deberías compartir la directiva, ¡podría ser útil!
Kim T
1
Aquí hay una adaptación del código para un Pen: codepen.io/anon/pen/qERdza ¡Espero que ayude! También alterna presionar la tecla ESC cuando tiene el video encendido
DD.
¡Esto parece demasiado bueno para ser verdad! ¿Existen limitaciones de navegador / seguridad para usar este método?
Dan
Sí, IE tiene algunas limitaciones, especialmente IE10 que admite MessageChannel en lugar de postMessage: caniuse.com/#search=postMessage también tenga en cuenta que las Políticas de seguridad de contenido también restringirán el uso de esta función
Kim T
5

Mi propia versión del código de Kim T anterior que se combina con algunos jQuery y permite la orientación de iframes específicos.

$(function() {
    callPlayer($('#iframe')[0], 'unMute');
});

function callPlayer(iframe, func, args) {
    if ( iframe.src.indexOf('youtube.com/embed') !== -1) {
        iframe.contentWindow.postMessage( JSON.stringify({
            'event': 'command',
            'func': func,
            'args': args || []
        } ), '*');
    }
}
adamj
fuente
¿Cómo saber que YouTube está jugando? alguna devolución de llamada desde el iframe de youtube, por lo que afuera puede suscribirse?
Martillo
@Hammer Echa un vistazo a la sección Eventos de API de YouTube específicamente OnStateChange: developers.google.com/youtube/iframe_api_reference#Events
adamj
@admj, ¿puedes ver esto? Un comportamiento extraño ... stackoverflow.com/questions/38389802/…
Martillo
0

Gracias Rob W por tu respuesta.

He estado usando esto dentro de una aplicación Cordova para evitar tener que cargar la API y poder controlar fácilmente los iframes que se cargan dinámicamente.

Siempre quise poder extraer información del iframe, como el estado (getPlayerState) y la hora (getCurrentTime).

Rob W ayudó a resaltar cómo funciona la API usando postMessage, pero, por supuesto, esto solo envía información en una dirección, desde nuestra página web al iframe. El acceso a los captadores requiere que escuchemos los mensajes publicados desde el iframe.

Me tomó algo de tiempo descubrir cómo ajustar la respuesta de Rob W para activar y escuchar los mensajes devueltos por el iframe. Básicamente busqué en el código fuente dentro del iframe de YouTube hasta que encontré el código responsable de enviar y recibir mensajes.

La clave fue cambiar el 'evento' a 'escuchar', esto básicamente dio acceso a todos los métodos que fueron diseñados para devolver valores.

A continuación se encuentra mi solución, tenga en cuenta que he cambiado a 'escuchar' solo cuando se solicitan captadores, puede modificar la condición para incluir métodos adicionales.

Tenga en cuenta además que puede ver todos los mensajes enviados desde el iframe agregando un console.log (e) al window.onmessage. Notará que una vez que se activa la escucha, recibirá actualizaciones constantes que incluyen la hora actual del video. Al llamar a getters como getPlayerState se activarán estas actualizaciones constantes, pero solo enviarán un mensaje que involucre el estado del video cuando el estado haya cambiado.

function callPlayer(iframe, func, args) {
    iframe=document.getElementById(iframe);
    var event = "command";
    if(func.indexOf('get')>-1){
        event = "listening";
    }

    if ( iframe&&iframe.src.indexOf('youtube.com/embed') !== -1) {
      iframe.contentWindow.postMessage( JSON.stringify({
          'event': event,
          'func': func,
          'args': args || []
      }), '*');
    }
}
window.onmessage = function(e){
    var data = JSON.parse(e.data);
    data = data.info;
    if(data.currentTime){
        console.log("The current time is "+data.currentTime);
    }
    if(data.playerState){
        console.log("The player state is "+data.playerState);
    }
}
Danbardo
fuente