开始投放设备的投放会话 [英] Start cast session for a cast device

查看:104
本文介绍了开始投放设备的投放会话的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有这个用例:

  1. 检测投射设备并保存其ID,名称和信息;
  2. 以自动方式连接到预定义的设备并开始投射会话 有一些内容.
  1. Detect cast devices and save their id, names and information;
  2. In automatic way, connect to predefined device and start cast session with some content.

我已经研究了Google Cast API v3,这看起来真的很难.使用v2时,由于发件人应用控制了90%的过程(即与设备和负载内容的连接),因此有可能,而使用v3时,会话完全由框架管理,并且仅在用户干预的情况下才开始会话.对于我的用例而言,唯一值得使用的方法是SessionManager.startSession(Intent intent)

I've studied the Google Cast API v3 and it seems really hard. While with v2 it was possible since the sender app controls 90% of the process, i.e. connection with device and load content, with v3 the session is totally managed by the framework and a session is started only whit user intervention. The only method that it can be worth for my use case is the SessionManager.startSession(Intent intent) doc here, however it's totally undocumented how to use the intent, extra parameters, action and so on. Is there anyone with some knowledge about this method and intent?

推荐答案

TLDR;跳至第3步-选项1 (SessionManager.startSession)或第3步-选项2 (MediaRouter.selectRoute)

TLDR; Skip to Step 3 - Option 1 (SessionManager.startSession) or Step 3 - Option 2 (MediaRouter.selectRoute)

像平常一样设置CastOptionsProvider.

Set up CastOptionsProvider like normal.

这是我们将要使用的主要对象:

Here are the main objects that we will use:

MediaRouter mediaRouter = MediaRouter.getInstance(activity);
CastContex context = CastContext.getSharedInstance(activity);
SessionManager sessionManager = context.getSessionManager();

第2步-检索用于保存/使用的路由(设备)

获取路由/设备ID

Step 2 - Retrieve Routes (Devices) for Saving/Using

Get the route/device Ids

只需获取当前缓存的路由:

Just get the current cached routes:

for (RouteInfo route : mediaRouter.getRoutes()) {
    // Save route.getId(); however you want (it's a string)
}

缺点:返回的路线可能已过时. MediaRouter的路由缓存仅在触发扫描(由您手动或由演员库)触发时更新.

Drawback: The returned routes may be well out of date. MediaRouter's cache of routes is only updated when a scan is triggered (by you manually, or by the cast library).

主动扫描以获取最准确的路线列表:

Active scan for the most accurate list of routes:

MediaRouter.Callback callback = new MediaRouter.Callback() {
    private void updateMyRouteList() {
        for (RouteInfo route : mediaRouter.getRoutes()) {
            // Save route.getId() however you want (it's a string)
        }
    }
    @Override
    public void onRouteAdded(MediaRouter router, RouteInfo route) {
        updateMyRouteList();
    }

    @Override
    public void onRouteRemoved(MediaRouter router, RouteInfo route) {
        updateMyRouteList();
    }
    @Override
    public void onRouteChanged(MediaRouter router, RouteInfo route) {
        updateMyRouteList();
    }
};
mediaRouter.addCallback(new MediaRouteSelector.Builder()
                .addControlCategory(CastMediaControlIntent.categoryForCast(appId))
                .build(),
        callback,
        MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);

注意!,重要的是停止主动扫描,否则电池会很快耗尽!您可以使用

NOTE! It is important that you stop the active scan or the battery will drain quickly! You stop the scan with

mediaRouter.removeCallback(callback);

步骤2-选项3-被动扫描

选项2 相同,但是省略mediaRouter.addCallbackflags自变量.
这个应该(我认为)是被动地监听路线变化. (尽管您可能没有比选项1 更好的结果).例如:

Step 2 - Option 3 - Passive Scan

Same as Option 2 but, omit the flags argument of mediaRouter.addCallback.
This should (I think) listen for route changes passively. (Though you may not have much better results than in Option 1). For example:

mediaRouter.addCallback(new MediaRouteSelector.Builder()
                .addControlCategory(CastMediaControlIntent.categoryForCast(appId))
                .build(),
        callback);

第3步-加入路由(设备)

如何以编程方式加入路线(设备). 有2个主要选项.

Step 3 - Join a Route (Device)

How to programmatically join a route (device). There are 2 main options.

这两个选项都将创建一个新会话,或者在您要加入的设备上加入现有的会话(如果appId相同).

Both options will either creating a new session, or joining an existing session on the device you are trying to join (if the appId is the same).

首先,准备:

// Optional - if your app changes receiverApplicationId on the fly you should change that here
context.setReceiverApplicationId(appId);
// Most people would just set this as a constant in their CastOptionsProvider

// Listen for a successful join
sessionManager.addSessionManagerListener(new SessionManagerListener<Session>() {
    @Override
    public void onSessionStarted(CastSession castSession, String sessionId) { 
        // We successfully joined a route(device)!
    }
});

现在,鉴于我们从第2步

注意:我发现此方法在我的Android 4.4设备上不起作用.我收到错误为15(超时)的SessionManagerListener.onSessionStartFailed.
不过,它确实可以在我的Android 7.0设备上工作.

NOTE: I found this method did not work on my Android 4.4 device. I was getting SessionManagerListener.onSessionStartFailed with error 15 (timeout).
It did work on my Android 7.0 device though.

// Create the intent
Intent castIntent = new Intent();
// Mandatory, if null, nothing will happen
castIntent.putExtra("CAST_INTENT_TO_CAST_ROUTE_ID_KEY", routeId);
// (Optional) Uses this name in the toast
castIntent.putExtra("CAST_INTENT_TO_CAST_DEVICE_NAME_KEY", route.getName());
// Optional - false = displays "Connecting to <devicename>..."
castIntent.putExtra("CAST_INTENT_TO_CAST_NO_TOAST_KEY", true);

sessionManager.startSession(castIntent);

步骤3-选项2-MediaRouter.selectRoute

要使用此选项,您必须具有完整的Route对象,而不仅仅是id字符串.
如果您已经有了对象,那就太好了!
如果不是,则可以使用步骤2-选项2-主动扫描中的方法,通过查找匹配的ID来获取Route对象.

Step 3 - Option 2 - MediaRouter.selectRoute

To use this option you have to have the full Route object, not just the id string.
If you already have the object, great!
If not, you can use the method in Step 2 - Option2 - Active Scan to get the Route object by looking for a matching id.

mediaRouter.selectRoute(routeObject);

步骤4-流内容

一旦您完成了第3步准备工作中的会话,辛苦的工作就完成了.
您可以使用
RemoteMediaClient 控制演员阵容.

Step 4 - Stream Content

Once you have the session from step 3 prep, the hard work is done.
You can use RemoteMediaClient to control what is casted.

RemoteMediaClient remoteMediaClient = castSession.getRemoteMediaClient();
remoteMediaClient.load(...);

完整代码

我将包括这一点,因为我花了很多时间来解决会话问题,并希望它可以使其他人受益. (包括Android 4.4/慢速设备上的间歇性计时和崩溃问题[不确定是问题的根源]).

The Full Code

I'm going to include this because I have spent ridiculous amounts of hours fighting with session issues and hopefully it can benefit someone else. (Including intermittent timing and crash issues on Android 4.4/Slow device [not sure which one is the source of the problems]).

其中可能有一些多余的东西(尤其是如果您使用恒定的appId,则initialize将是无关紧要的),因此请使用所需的东西.

There is probably some extra stuff in there (especially if you use a constant appId, initialize will be irrelevant), so please use what you need.

最相关的方法是selectRoute,它接受routeId字符串,并将主动扫描匹配项长达15秒.重试我的工作时还会处理一些错误.

The method of most relevance is selectRoute which accepts a routeId string and will actively scan for the matching for up to 15 seconds. It also handles some errors where a retry my work.

您可以看到真正的完整代码在这里.

public class ChromecastConnection {

    /** Lifetime variable. */
    private Activity activity;
    /** settings object. */
    private SharedPreferences settings;

    /** Lifetime variable. */
    private SessionListener newConnectionListener;
    /** The Listener callback. */
    private Listener listener;

    /** Initialize lifetime variable. */
    private String appId;

    /**
     * Constructor.  Call this in activity start.
     * @param act the current context
     * @param connectionListener client callbacks for specific events
     */
    ChromecastConnection(Activity act, Listener connectionListener) {
        this.activity = act;
        this.settings = activity.getSharedPreferences("CORDOVA-PLUGIN-CHROMECAST_ChromecastConnection", 0);
        this.appId = settings.getString("appId", CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID);
        this.listener = connectionListener;

        // Set the initial appId
        CastOptionsProvider.setAppId(appId);

        // This is the first call to getContext which will start up the
        // CastContext and prep it for searching for a session to rejoin
        // Also adds the receiver update callback
        getContext().addCastStateListener(listener);
    }

    /**
     * Must be called each time the appId changes and at least once before any other method is called.
     * @param applicationId the app id to use
     * @param callback called when initialization is complete
     */
    public void initialize(String applicationId, CallbackContext callback) {
        activity.runOnUiThread(new Runnable() {
            public void run() {

                // If the app Id changed, set it again
                if (!applicationId.equals(appId)) {
                    setAppId(applicationId);
                }

                // Tell the client that initialization was a success
                callback.success();

                // Check if there is any available receivers for 5 seconds
                startRouteScan(5000L, new ScanCallback() {
                    @Override
                    void onRouteUpdate(List<RouteInfo> routes) {
                        // if the routes have changed, we may have an available device
                        // If there is at least one device available
                        if (getContext().getCastState() != CastState.NO_DEVICES_AVAILABLE) {
                            // Stop the scan
                            stopRouteScan(this);
                            // Let the client know a receiver is available
                            listener.onReceiverAvailableUpdate(true);
                            // Since we have a receiver we may also have an active session
                            CastSession session = getSessionManager().getCurrentCastSession();
                            // If we do have a session
                            if (session != null) {
                                // Let the client know
                                listener.onSessionRejoin(session);
                            }
                        }
                    }
                }, null);
            }
        });
    }

    private MediaRouter getMediaRouter() {
        return MediaRouter.getInstance(activity);
    }

    private CastContext getContext() {
        return CastContext.getSharedInstance(activity);
    }

    private SessionManager getSessionManager() {
        return getContext().getSessionManager();
    }

    private CastSession getSession() {
        return getSessionManager().getCurrentCastSession();
    }

    private void setAppId(String applicationId) {
        this.appId = applicationId;
        this.settings.edit().putString("appId", appId).apply();
        getContext().setReceiverApplicationId(appId);
    }

    /**
     * This will create a new session or seamlessly selectRoute an existing one if we created it.
     * @param routeId the id of the route to selectRoute
     * @param callback calls callback.onJoin when we have joined a session,
     *                 or callback.onError if an error occurred
     */
    public void selectRoute(final String routeId, SelectRouteCallback callback) {
        activity.runOnUiThread(new Runnable() {
            public void run() {
                if (getSession() != null && getSession().isConnected()) {
                    callback.onError(ChromecastUtilities.createError("session_error",
                            "Leave or stop current session before attempting to join new session."));
                }

                // We need this hack so that we can access these values in callbacks without having
                // to store it as a global variable, just always access first element
                final boolean[] foundRoute = {false};
                final boolean[] sentResult = {false};
                final int[] retries = {0};

                // We need to start an active scan because getMediaRouter().getRoutes() may be out
                // of date.  Also, maintaining a list of known routes doesn't work.  It is possible
                // to have a route in your "known" routes list, but is not in
                // getMediaRouter().getRoutes() which will result in "Ignoring attempt to select
                // removed route: ", even if that route *should* be available.  This state could
                // happen because routes are periodically "removed" and "added", and if the last
                // time media router was scanning ended when the route was temporarily removed the
                // getRoutes() fn will have no record of the route.  We need the active scan to
                // avoid this situation as well.  PS. Just running the scan non-stop is a poor idea
                // since it will drain battery power quickly.
                ScanCallback scan = new ScanCallback() {
                    @Override
                    void onRouteUpdate(List<RouteInfo> routes) {
                        // Look for the matching route
                        for (RouteInfo route : routes) {
                            if (!foundRoute[0] && route.getId().equals(routeId)) {
                                // Found the route!
                                foundRoute[0] = true;
                                // try-catch for issue:
                                // https://github.com/jellyfin/cordova-plugin-chromecast/issues/48
                                try {
                                    // Try selecting the route!
                                    getMediaRouter().selectRoute(route);
                                } catch (NullPointerException e) {
                                    // Let it try to find the route again
                                    foundRoute[0] = false;
                                }
                            }
                        }
                    }
                };

                Runnable retry = new Runnable() {
                    @Override
                    public void run() {
                        // Reset foundRoute
                        foundRoute[0] = false;
                        // Feed current routes into scan so that it can retry.
                        // If route is there, it will try to join,
                        // if not, it should wait for the scan to find the route
                        scan.onRouteUpdate(getMediaRouter().getRoutes());
                    }
                };

                Function<JSONObject, Void> sendErrorResult = new Function<JSONObject, Void>() {
                    @Override
                    public Void apply(JSONObject message) {
                        if (!sentResult[0]) {
                            sentResult[0] = true;
                            stopRouteScan(scan);
                            callback.onError(message);
                        }
                        return null;
                    }
                };

                listenForConnection(new ConnectionCallback() {
                    @Override
                    public void onJoin(CastSession session) {
                        sentResult[0] = true;
                        stopRouteScan(scan);
                        callback.onJoin(session);
                    }
                    @Override
                    public boolean onSessionStartFailed(int errorCode) {
                        if (errorCode == 7 || errorCode == 15) {
                            // It network or timeout error retry
                            retry.run();
                            return false;
                        } else {
                            sendErrorResult.apply(ChromecastUtilities.createError("session_error",
                                    "Failed to start session with error code: " + errorCode));
                            return true;
                        }
                    }
                    @Override
                    public boolean onSessionEndedBeforeStart(int errorCode) {
                        if (retries[0] < 10) {
                            retries[0]++;
                            retry.run();
                            return false;
                        } else {
                            sendErrorResult.apply(ChromecastUtilities.createError("session_error",
                                    "Failed to to join existing route (" + routeId + ") " + retries[0] + 1 + " times before giving up."));
                            return true;
                        }
                    }
                });

                startRouteScan(15000L, scan, new Runnable() {
                    @Override
                    public void run() {
                        sendErrorResult.apply(ChromecastUtilities.createError("timeout",
                                "Failed to to join route (" + routeId + ") after 15s and " + retries[0] + 1 + " trys."));
                    }
                });
            }
        });
    }

    /**
     * Must be called from the main thread.
     * @param callback calls callback.success when we have joined, or callback.error if an error occurred
     */
    private void listenForConnection(ConnectionCallback callback) {
        // We should only ever have one of these listeners active at a time, so remove previous
        getSessionManager().removeSessionManagerListener(newConnectionListener, CastSession.class);
        newConnectionListener = new SessionListener() {
            @Override
            public void onSessionStarted(CastSession castSession, String sessionId) {
                getSessionManager().removeSessionManagerListener(this, CastSession.class);
                callback.onJoin(castSession);
            }
            @Override
            public void onSessionStartFailed(CastSession castSession, int errCode) {
                if (callback.onSessionStartFailed(errCode)) {
                    getSessionManager().removeSessionManagerListener(this, CastSession.class);
                }
            }
            @Override
            public void onSessionEnded(CastSession castSession, int errCode) {
                if (callback.onSessionEndedBeforeStart(errCode)) {
                    getSessionManager().removeSessionManagerListener(this, CastSession.class);
                }
            }
        };
        getSessionManager().addSessionManagerListener(newConnectionListener, CastSession.class);
    }

    /**
     * Starts listening for receiver updates.
     * Must call stopRouteScan(callback) or the battery will drain with non-stop active scanning.
     * @param timeout ms until the scan automatically stops,
     *                if 0 only calls callback.onRouteUpdate once with the currently known routes
     *                if null, will scan until stopRouteScan is called
     * @param callback the callback to receive route updates on
     * @param onTimeout called when the timeout hits
     */
    public void startRouteScan(Long timeout, ScanCallback callback, Runnable onTimeout) {
        // Add the callback in active scan mode
        activity.runOnUiThread(new Runnable() {
            public void run() {
                callback.setMediaRouter(getMediaRouter());

                if (timeout != null && timeout == 0) {
                    // Send out the one time routes
                    callback.onFilteredRouteUpdate();
                    return;
                }

                // Add the callback in active scan mode
                getMediaRouter().addCallback(new MediaRouteSelector.Builder()
                        .addControlCategory(CastMediaControlIntent.categoryForCast(appId))
                        .build(),
                        callback,
                        MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);

                // Send out the initial routes after the callback has been added.
                // This is important because if the callback calls stopRouteScan only once, and it
                // happens during this call of "onFilterRouteUpdate", there must actually be an
                // added callback to remove to stop the scan.
                callback.onFilteredRouteUpdate();

                if (timeout != null) {
                    // remove the callback after timeout ms, and notify caller
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            // And stop the scan for routes
                            getMediaRouter().removeCallback(callback);
                            // Notify
                            if (onTimeout != null) {
                                onTimeout.run();
                            }
                        }
                    }, timeout);
                }
            }
        });
    }

    /**
     * Call to stop the active scan if any exist.
     * @param callback the callback to stop and remove
     */
    public void stopRouteScan(ScanCallback callback) {
        activity.runOnUiThread(new Runnable() {
            public void run() {
                callback.stop();
                getMediaRouter().removeCallback(callback);
            }
        });
    }

    /**
     * Create this empty class so that we don't have to override every function
     * each time we need a SessionManagerListener.
     */
    private class SessionListener implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession castSession) { }
        @Override
        public void onSessionStarted(CastSession castSession, String sessionId) { }
        @Override
        public void onSessionStartFailed(CastSession castSession, int error) { }
        @Override
        public void onSessionEnding(CastSession castSession) { }
        @Override
        public void onSessionEnded(CastSession castSession, int error) { }
        @Override
        public void onSessionResuming(CastSession castSession, String sessionId) { }
        @Override
        public void onSessionResumed(CastSession castSession, boolean wasSuspended) { }
        @Override
        public void onSessionResumeFailed(CastSession castSession, int error) { }
        @Override
        public void onSessionSuspended(CastSession castSession, int reason) { }
    }

    interface SelectRouteCallback {
        void onJoin(CastSession session);
        void onError(JSONObject message);
    }

    interface ConnectionCallback {
        /**
         * Successfully joined a session on a route.
         * @param session the session we joined
         */
        void onJoin(CastSession session);

        /**
         * Called if we received an error.
         * @param errorCode You can find the error meaning here:
         *                 https://developers.google.com/android/reference/com/google/android/gms/cast/CastStatusCodes
         * @return true if we are done listening for join, false, if we to keep listening
         */
        boolean onSessionStartFailed(int errorCode);

        /**
         * Called when we detect a session ended event before session started.
         * See issues:
         *     https://github.com/jellyfin/cordova-plugin-chromecast/issues/49
         *     https://github.com/jellyfin/cordova-plugin-chromecast/issues/48
         * @param errorCode error to output
         * @return true if we are done listening for join, false, if we to keep listening
         */
        boolean onSessionEndedBeforeStart(int errorCode);
    }

    public abstract static class ScanCallback extends MediaRouter.Callback {
        /**
         * Called whenever a route is updated.
         * @param routes the currently available routes
         */
        abstract void onRouteUpdate(List<RouteInfo> routes);

        /** records whether we have been stopped or not. */
        private boolean stopped = false;
        /** Global mediaRouter object. */
        private MediaRouter mediaRouter;

        /**
         * Sets the mediaRouter object.
         * @param router mediaRouter object
         */
        void setMediaRouter(MediaRouter router) {
            this.mediaRouter = router;
        }

        /**
         * Call this method when you wish to stop scanning.
         * It is important that it is called, otherwise battery
         * life will drain more quickly.
         */
        void stop() {
            stopped = true;
        }
        private void onFilteredRouteUpdate() {
            if (stopped || mediaRouter == null) {
                return;
            }
            List<RouteInfo> outRoutes = new ArrayList<>();
            // Filter the routes
            for (RouteInfo route : mediaRouter.getRoutes()) {
                // We don't want default routes, or duplicate active routes
                // or multizone duplicates https://github.com/jellyfin/cordova-plugin-chromecast/issues/32
                Bundle extras = route.getExtras();
                if (extras != null) {
                    CastDevice.getFromBundle(extras);
                    if (extras.getString("com.google.android.gms.cast.EXTRA_SESSION_ID") != null) {
                        continue;
                    }
                }
                if (!route.isDefault()
                        && !route.getDescription().equals("Google Cast Multizone Member")
                        && route.getPlaybackType() == RouteInfo.PLAYBACK_TYPE_REMOTE
                ) {
                    outRoutes.add(route);
                }
            }
            onRouteUpdate(outRoutes);
        }
        @Override
        public final void onRouteAdded(MediaRouter router, RouteInfo route) {
            onFilteredRouteUpdate();
        }
        @Override
        public final void onRouteChanged(MediaRouter router, RouteInfo route) {
            onFilteredRouteUpdate();
        }
        @Override
        public final void onRouteRemoved(MediaRouter router, RouteInfo route) {
            onFilteredRouteUpdate();
        }
    }

    abstract static class Listener implements CastStateListener {
        abstract void onReceiverAvailableUpdate(boolean available);
        abstract void onSessionRejoin(CastSession session);

        /** CastStateListener functions. */
        @Override
        public void onCastStateChanged(int state) {
            onReceiverAvailableUpdate(state != CastState.NO_DEVICES_AVAILABLE);
        }
    }

}

使用chromecast很有趣...

Working with chromecast is so fun...

这篇关于开始投放设备的投放会话的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆