Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot sendMessage() to cast URL #78

Open
enricodente opened this issue Jan 24, 2021 · 6 comments
Open

Cannot sendMessage() to cast URL #78

enricodente opened this issue Jan 24, 2021 · 6 comments

Comments

@enricodente
Copy link

Hello and thanks for this amazing plugin.

I have set it up and got the example with the video and pause/stop working in few minutes.

Unfortunately, that is not the use I would like to achieve: I would need to open a remote URL on the screen. I have found this project that uses Chromecast APIs and works like a charm on their web-example using Chrome browser:
Project: https://github.com/DeMille/url-cast-receiver
Example: https://demille.github.io/url-cast-receiver/

Basically, I would like to achieve that using this plugin.
I have tried to adapt their implementation inside my test cordova app, but I always get issues when calling the requestSession method: the error callback gets triggered, and the error I get is: "namespace 'urn:x-cast:com.url.cast' not founded unverified"

I have tried both using the recommended App ID and namespace from the project, and also tried using one generated by me, but always get that error. After that, it looks like I can no longer close the session, not even force closing the app, so the only way to be able to scan again my Chromecast TV is to uninstall the app and re-install again, but I believe this is only a side effect of the main issue.

Thanks for any hint!

@Lindsay-Needs-Sleep
Copy link
Collaborator

I'm not totally sure, but you can try pasting the code below to the console. It should ask you to choose a chromecast, then open example.com on your chromecast. (worked for me on android).

This is just a slight modification of example.js from the home page. (I have marked the parts I changed with /* DeMille so you can find them easier.)

One note, it looks like the DeMilla stuff relies on custom messages. The client-side sendMessage API is technically provided by this plugin, but it completely untested, (pretty sure there is at least 1 bug).
So if you have any trouble it could be related to that.
The native code that is relative to the sendMessage function is pretty small, so it shouldn't be too bad to fix if needed. (If you do that, please do submit a PR!)

document.addEventListener("deviceready", function () {
    // Must wait for deviceready before using chromecast

    // File globals
    var _session;
    var _media;

    initialize();

    function initialize () {
        // use default app id
/* DeMille START1 */
        var appId = "5CB45E5A"; // --> You have to use DeMille's app id to send messages to their receiver
        // var appId = chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID;
/* DeMille END1 */
        var apiConfig = new chrome.cast.ApiConfig(new chrome.cast.SessionRequest(appId), function sessionListener (session) {
                // The session listener is only called under the following conditions:
                // * will be called shortly chrome.cast.initialize is run
                // * if the device is already connected to a cast session
                // Basically, this is what allows you to re-use the same cast session 
                // across different pages and after app restarts
            }, function receiverListener (receiverAvailable) {
                // receiverAvailable is a boolean.
                // True = at least one chromecast device is available
                // False = No chromecast devices available
                // You can use this to determine if you want to show your chromecast icon
            });

        // initialize chromecast, this must be done before using other chromecast features
        chrome.cast.initialize(apiConfig, function () {
            // Initialize complete
            // Let's start casting
            requestSession();
        }, function (err) {
            // Initialize failure
        });
    }


    function requestSession () {
        // This will open a native dialog that will let 
        // the user choose a chromecast to connect to
        // (Or will let you disconnect if you are already connected)
        chrome.cast.requestSession(function (session) {
            // Got a session!
            _session = session;

/* DeMille START2 */
    var namespace = 'urn:x-cast:com.url.cast';
    var msg = {
        "type": "loc",
        "url": "http://example.com"
    }
    session.sendMessage(namespace, msg, onSuccess, onErr);

    // Load a video            
    //loadMedia();
/* DeMille END2 */
        }, function (err) {
            // Failed, or if err is cancel, the dialog closed
        });
    }

    function loadMedia () {
        var videoUrl = 'https://ia801302.us.archive.org/1/items/TheWater_201510/TheWater.mp4';
        var mediaInfo = new chrome.cast.media.MediaInfo(videoUrl, 'video/mp4');

        _session.loadMedia(new chrome.cast.media.LoadRequest(mediaInfo), function (media) {
            // You should see the video playing now!
            // Got media!
            _media = media;

            // Wait a couple seconds
            setTimeout(function () {
                // Lets pause the media
                pauseMedia();
            }, 4000);

        }, function (err) {
            // Failed (check that the video works in your browser)
        });
    }

    function pauseMedia () {
        _media.pause({}, function () {
            // Success
            
            // Wait a couple seconds
            setTimeout(function () {
                // stop the session
                stopSession();
            }, 2000)

        }, function (err) {
            // Fail
        });
    }

    function stopSession () {
        // Also stop the session (if )
        _session.stop(function () {
            // Success
        }, function (err) {
            // Fail
        });
    }

});

@enricodente
Copy link
Author

Hi Lindsay, thanks for your great answer!

In my previous attempts I did exactly what you wrote in your code, but I got the error on the namespace.

I was testing on iOS. I will try again on Android where you had a working experience, but right now I am experiencing build issues (not related to this plugin) so it is taking me some more time to provide feedback.

I will do come back here as soon as I will be able to test on Android platform.

@enricodente
Copy link
Author

enricodente commented Jan 27, 2021

Hello Lindsay! I managed to test on Android, and it worked at the first time with the same code of iOS... there must be some trouble with sendMessage() method in iOS implementation only.

More specifically, the error I get "Namespace not founded, undefined", only appears into this piece of code in MLPChromecastSession.m (ios folder):

- (void)sendMessageWithCommand:(CDVInvokedUrlCommand*)command namespace:(NSString*)namespace message:(NSString*)message {
    GCKGenericChannel* channel = self.genericChannels[namespace];
    CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[NSString stringWithFormat:@"Namespace %@ not founded",namespace]];
    
    if (channel != nil) {
        GCKError* error = nil;
        [channel sendTextMessage:message error:&error];
        if (error != nil) {
            pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:error.description];
        } else {
            pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
        }
    }
    
    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

I am not expert in Swift at all, but I believe that this means that "channel" variable is "nil", so the pluginResult keeps that value. Do you have any idea why this is not working as expected?

@Lindsay-Needs-Sleep
Copy link
Collaborator

@enricodente Sorry for the slow reply (I didn't notice the notification >.<)

I kind of suspected that the problem might be the incomplete/untested implementation. >.<

(Just a side note, the ios implementation is written in obj-c, just incase you try to google anything)


I don't have this project setup for development atm, so everything I am saying is just based on looking at the code. (so accuracy might be low xD)

Probably the easiest way to find out what is wrong (if you have xcode):

  • put a breakpoint in sendMessage (MLPChreomecast.m) (I believe namespace should be non-nil to function correctly as you have mentioned)
  • put a breakpoint in the equivalent android/java function sendMessage ('Chromecast.java`)
  • The 2 functions I referenced are basically the native entrypoints for these methods
    • Their main purpose is to extract the arguments and then call the function that actually does the work
  • I would see the ios implementation is receiving the same arguments as android first
    • If they are, you might need to step into the lower functions to see what's going wrong

Honestly, I'm not an obj-c expert either, but my guess is that the problem line is indeed:

GCKGenericChannel* channel = self.genericChannels[namespace];

I'm guessing this channel doesn't exist in the "genereicChannels" (whatever they are), and maybe we need to make a call to createMessageChannelWithCommand if self.genericChannels[namespace]; returns nil. so maybe something like this:

- (void)createMessageChannelWithCommand:(CDVInvokedUrlCommand*)command namespace:(NSString*)namespace{
    GCKGenericChannel* newChannel = [self getMessageChannel:namespace];
    CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

- (GCKGenericChannel*)getMessageChannel:(NSString*)namespace{
    GCKGenericChannel* channel = self.genericChannels[namespace];
    if (channel != nil) {
        return channel;
    }
    GCKGenericChannel* newChannel = [[GCKGenericChannel alloc] initWithNamespace:namespace];
    newChannel.delegate = self;
    self.genericChannels[namespace] = newChannel;
    [currentSession addChannel:newChannel];
    return newChannel;
}

- (void)sendMessageWithCommand:(CDVInvokedUrlCommand*)command namespace:(NSString*)namespace message:(NSString*)message {
    GCKGenericChannel* channel = [self getMessageChannel:namespace];
    CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[NSString stringWithFormat:@"Namespace %@ not founded",namespace]];
    
    if (channel != nil) {
        GCKError* error = nil;
        [channel sendTextMessage:message error:&error];
        if (error != nil) {
            pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:error.description];
        } else {
            pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
        }
    }
    
    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
  • Should replace from the createMessageChannelWithCommand function to sendMessageWithCommand with this
  • Might need to add getMessageChannel to MLPChromecastSession.h
  • almost definitely will need to fix something in my syntax (can't hardly remember any objc right now, and wrote this in notepad)

Let me know how it goes!

@enricodente
Copy link
Author

Hi Lindsay,

I kind of solved the issue, and I believe we came up with a similar solution. Yours looks better in terms of implementation, mine is more of an hack, but I confirm I tested it yesterday and it works:

- (void)sendMessageWithCommand:(CDVInvokedUrlCommand*)command namespace:(NSString*)namespace message:(NSString*)message{

    GCKGenericChannel* newChannel = [[GCKGenericChannel alloc] initWithNamespace:namespace];
    newChannel.delegate = self;
    self.genericChannels[namespace] = newChannel;
    [currentSession addChannel:newChannel];

    CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[NSString stringWithFormat:@"Namespace %@ not found",namespace]];
    
    if(newChannel != nil) {
        GCKError* error = nil;
        [newChannel sendTextMessage:message error:&error];
        if (error != nil) {
            pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:error.description];
        } else {
            pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
        }
    }
    
    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

If you confirm your solution is cleaner and would be worth a PR, I can try to implement it your way, test it to be working, and create a PR. Can you approve PRs on this repository, or should I do it on miloproductionsinc's?

@Lindsay-Needs-Sleep
Copy link
Collaborator

I am glad to hear you got it to work! (Nice job!)

It looks like we were effectively doing the same thing!

The only difference is the solution I wrote caches that channel instead of creating it each time. I think if this works, it might be slightly better. (Not sure if maybe you actually need to create the channel each time or not though :p)

I'm not sure if I can approve PR's here. I was given some permissions... I know for sure I can't approve my own PR. :p

For sure, you should definitely branch off of miloproductionsinc. (I'm guessing you already did)

I think more people find this repo first though. So maybe you can submit to both? If I can approve your PR on Jellyfin then that will update this repo too which would be good for everyone else. If not, I will definitely accept it on miloproductionsinc!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants