Chromecast custom CAF receiver

2k Views Asked by At

I'm having issues with the new CAF receiver not registering the YouTube iframe player as a player, and that it is playing. After 5 minutes connected to the receiver, the connection is broken because it thinks that the player is idle.

This is the sender-code

var metadata = new chrome.cast.media.GenericMediaMetadata();
metadata.title = "Foo - Bar";
metadata.image = 'https://img.youtube.com/vi/IXNrHusLXoM/mqdefault.jpg';
metadata.images = ['https://img.youtube.com/vi/IXNrHusLXoM/mqdefault.jpg'];
var mediaInfo = new chrome.cast.media.MediaInfo();
mediaInfo.contentType = "video/*";
mediaInfo.contentId ="IXNrHusLXoM";
mediaInfo.duration = 300;
var request = new chrome.cast.media.LoadRequest();
request.media = mediaInfo;
request.customData = customData;
request.metadata = metadata;
castSession.loadMedia(request).then(
  function() {
      console.log('Load succeed');
  },
  function(errorCode) {
      console.log('Error code: ' + errorCode);
});

The receiver code can be found here: https://github.com/zoff-music/zoff-chromecast-receiver/blob/feature/v3/receiver.js

Is there any way of having the new CAF receiver hook into the YouTube iframe player, or "manually" dispatch LOADED, BUFFERING, PLAYING, PAUSED, STOPPED events so that the receiver doesn't disconnect from the sender?

Edit: with the above code, the PlayerState gets to the BUFFERING stage, but stops there. The promise with the log "Load succeed" is never triggered.

2

There are 2 best solutions below

2
On

I managed to trick the receiver with fake mediaEelement. You can see the code in pastebin

const context = cast.framework.CastReceiverContext.getInstance();
const playerManager = context.getPlayerManager();

var yt_events = {};
var pause_request = false;
var yt_player;

var yt_video_fake = {
    removeAttribute: function(attr) {
    },
    setAttribute: function(attr, val) {
    },

    getCurrentTimeSec: function() { return yt_player && yt_player.getCurrentTime ? yt_player.getCurrentTime() : 0; },
    getDurationSec: function() { return yt_player ? yt_player.getDuration() : 0; },
    getVolume: function() {
        if(!yt_player || !yt_player.getVolume) {
            return 0;
        }

        var volume = new cast.framework.messages.Volume();

        volume.level = yt_player.getVolume() / 100;
        volume.muted = yt_player.isMuted() ? true : false;

        return volume;
    },
    setVolume: function(vol) { yt_player && yt_player.setVolume(vol.level * 100); },
    getState: function() {
        if(!yt_player || !yt_player.getPlayerState) {
            return 'IDLE';
        }

        var state = yt_player.getPlayerState();
        var _state;

        if(pause_request) {
            pause_request = false;
            state = YT.PlayerState.PAUSED;
        }

        switch(state) {
            default: case YT.PlayerState.UNSTARTED:
                _state = 'IDLE';
            break;

            case YT.PlayerState.PLAYING:
                _state = 'PLAYING';
            break;

            case YT.PlayerState.PAUSED:
                _state = 'PAUSED';
            break;

            case YT.PlayerState.BUFFERING:
                _state = 'BUFFERING';
            break;

            case YT.PlayerState.ENDED:
                _state = 'ENDED';
            break;
        }

        return _state;
    },

    addEventListener: function(e, func) { },
    load: function() {},
    play: function() { yt_player && yt_player.playVideo(); },
    pause: function() { if(yt_player && yt_player.pauseVideo) {pause_request = true; yt_player.pauseVideo(); }},
    seek: function(timeTo) { yt_player && yt_player.seekTo(timeTo, true);},
    reset: function() {
        if(yt_player) {
            try { yt_player.destroy && yt_player.destroy(); } catch(e) {
                //console.trace(e);
            };
            delete yt_player;
        }
    },

    registerErrorCallback: function(func) { yt_events['error'] = func; },
    registerEndedCallback: function(func) { yt_events['ended'] = func; },
    registerLoadCallback: function(func) { yt_events['load'] = func; },

    unregisterErrorCallback: function () { delete yt_events['error'] },
    unregisterEndedCallback: function () { delete yt_events['ended']},
    unregisterLoadCallback: function () { delete yt_events['load']}
};

Object.defineProperty(yt_video_fake, 'currentTime', {
    val1: null,
    get: function() { return yt_player && yt_player.getCurrentTime ? yt_player.getCurrentTime() : this.val1; },
    set: function(newValue) {
        yt_player && yt_player.seekTo(newValue, true);
        this.val1 = newValue;
    },
    enumerable      : true,
    configurable    : true
});

Object.defineProperty(yt_video_fake, 'volume', {
    val1: null,
    get: function() { var vol = this.getVolume(); if(vol) return vol['level']; return 1; },
    set: function(newValue) {
        yt_player && yt_player.setVolume && yt_player.setVolume(newValue * 100);
        this.val1 = newValue;
    },
    enumerable      : true,
    configurable    : true
});


Object.defineProperty(yt_video_fake, 'duration', {
    val1: null,
    get: function() { return this.getDurationSec(); },
    set: function() {},
    enumerable      : true,
    configurable    : true
});


function YoutubePlayMedia(videoid) {
    var yt_container = $('#yt_container');

    if(!yt_container.length) {
        yt_container = $('<div id="yt_container" style="position:absolute;top:0;left:0;width:100%;height:100%;"></div>');
        $('body').append(yt_container);
    }

    yt_container.html('<iframe id="youtube_container" style="width:100%;height:100%;" frameborder="0" allowfullscreen="1" allow="autoplay; encrypted-media" title="YouTube video player" src="//www.youtube.com/embed/' + videoid +'?autoplay=1&enablejsapi=1&modestbranding=1&controls=0&fs=0&iv_load_policy=3&rel=0&cc_load_policy=1&cc_lang_pref=bg"></iframe>');

    yt_player = new YT.Player('youtube_container', {
        events: {
            'onReady': function(e) {
                yt_player.is_loaded = true;
                yt_player.playVideo();
            },
            'onStateChange': function(e) {
                switch(e.data) {
                    case YT.PlayerState.PLAYING:
                        if(yt_player.is_loaded) {
                            if(yt_events['load']) {
                                yt_events['load']();
                            }
                        }

                    break;

                    case YT.PlayerState.ENDED:
                        //yt_events['ended'] && yt_events['ended'](e);
                    break;
                }
            },
            'onError': function(e) {
                //yt_events['error'] && yt_events['error'](e);
            }
        }
    });
}

function YoutubeLoadMedia(url) {
    current_media_type = 'Youtube';

    window.onYouTubeIframeAPIReady = function() {
        window.youtube_loaded = true;
        YoutubePlayMedia(url);
    }

    if(!window.youtube_script) {
        window.youtube_script = document.createElement('script');
        window.youtube_script.src = "https://www.youtube.com/iframe_api";
        var firstScriptTag = document.getElementsByTagName('script')[0];
        firstScriptTag.parentNode.insertBefore(window.youtube_script, firstScriptTag);
    } else {
        // Вече имаме api направо действаме
        YoutubePlayMedia(url);
    }
}


playerManager.setMessageInterceptor(
    cast.framework.messages.MessageType.LOAD,
    loadRequestData => {
        if (loadRequestData.media && loadRequestData.media.contentId) {
            YoutubeLoadMedia(loadRequestData.media.contentId);
            playerManager.setMediaElement(yt_video_fake);

            return false;
        }

        return loadRequestData;
    }
);

const options = new cast.framework.CastReceiverOptions();
options.disableIdleTimeout = true;

context.start(options);
1
On

The YouTube iframe player is not designed to be a player for a Cast receiver.