【问题标题】:Custom google cast receiver stuck in "Load is in progress"自定义 google cast 接收器卡在“正在加载”
【发布时间】:2020-02-05 19:44:57
【问题描述】:

我的自定义 v3 CAF 接收器应用已成功播放前几个直播和 vod 资产。之后,它进入一种状态,即媒体命令正在排队,因为“正在加载”。它仍在(成功地)获取清单,但 MEDIA_STATUS 仍处于“缓冲”状态。然后日志显示:

[ 4.537s] [cast.receiver.MediaManager] 加载正在进行中,媒体命令正在排队。

[ 5.893s] [cast.receiver.MediaManager] 缓冲状态改变,isPlayerBuffering: true old time: 0 current time: 0

[ 5.897s] [cast.receiver.MediaManager] 发送广播状态消息

CastContext 核心事件:{"type":"MEDIA_STATUS","mediaStatus":{"mediaSessionId":1,"playbackRate":1,"playerState":"BUFFERING","currentTime":0,"supportedMediaCommands" :12303,"volume":{"level":1,"muted":false},"currentItemId":1,"repeatMode":"REPEAT_OFF","liveSeekableRange":{"start":0,"end": 20.000999927520752,"isMovingWindow":true,"isLiveDone":false}}}

CastContext MEDIA_STATUS 事件:{"type":"MEDIA_STATUS","mediaStatus":{"mediaSessionId":1,"playbackRate":1,"playerState":"BUFFERING","currentTime":0,"supportedMediaCommands" :12303,"volume":{"level":1,"muted":false},"currentItemId":1,"repeatMode":"REPEAT_OFF","liveSeekableRange":{"start":0,"end": 20.000999927520752,"isMovingWindow":true,"isLiveDone":false}}}

获取完成加载:GET "(manifest url)"。

没有显示错误。

即使在关闭并重新启动投射会话之后,问题仍然存在。必须重新启动投射设备本身才能解决它。看起来数据在会话之间保留。

请务必注意,演员接收器应用尚未发布。它托管在本地网络上。

我的问题是:

  • 此卡住行为的原因可能是什么?
  • 会话之间是否保留任何会话数据?
  • 如何完全重置投射接收器应用,而无需重新启动投射设备。

接收器应用程序本身非常基础。除了许可包装之外,它类似于香草示例应用程序:

const { cast } = window;

const TAG = "CastContext";

class CastStore {
  static instance = null;

  error = observable.box();

  framerate = observable.box();

  static getInstance() {
    if (!CastStore.instance) {
      CastStore.instance = new CastStore();
    }
    return CastStore.instance;
  }

  get debugLog() {
    return this.framerate.get();
  }

  get errorLog() {
    return this.error.get();
  }

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

    playerManager.addEventListener(
      cast.framework.events.category.CORE,
      event => {
        console.log(TAG, "Core event: " + JSON.stringify(event));
      }
    );
    playerManager.addEventListener(
      cast.framework.events.EventType.MEDIA_STATUS,
      event => {
        console.log(TAG, "MEDIA_STATUS event: " + JSON.stringify(event));
      }
    );
    playerManager.addEventListener(
      cast.framework.events.EventType.BITRATE_CHANGED,
      event => {
        console.log(TAG, "BITRATE_CHANGED event: " + JSON.stringify(event));
        runInAction(() => {
          this.framerate.set(`bitrate: ${event.totalBitrate}`);
        });
      }
    );
    playerManager.addEventListener(
      cast.framework.events.EventType.ERROR,
      event => {
        console.log(TAG, "ERROR event: " + JSON.stringify(event));
        runInAction(() => {
          this.error.set(`Error detailedErrorCode: ${event.detailedErrorCode}`);
        });
      }
    );

    // intercept the LOAD request to be able to read in a contentId and get data.
    this.loadHandler = new LoadHandler();
    playerManager.setMessageInterceptor(
      cast.framework.messages.MessageType.LOAD,
      loadRequestData => {
        this.framerate.set(null);
        this.error.set(null);

        console.log(TAG, "LOAD message: " + JSON.stringify(loadRequestData));
        if (!loadRequestData.media) {
          const error = new cast.framework.messages.ErrorData(
            cast.framework.messages.ErrorType.LOAD_CANCELLED
          );
          error.reason = cast.framework.messages.ErrorReason.INVALID_PARAM;
          return error;
        }

        if (!loadRequestData.media.entity) {
          // Copy the value from contentId for legacy reasons if needed
          loadRequestData.media.entity = loadRequestData.media.contentId;
        }

        // notify loadMedia
        this.loadHandler.onLoadMedia(loadRequestData, playerManager);
        return loadRequestData;
      }
    );

    const playbackConfig = new cast.framework.PlaybackConfig();

    // intercept license requests & responses
    playbackConfig.licenseRequestHandler = requestInfo => {
      const challenge = requestInfo.content;
      const { castToken } = this.loadHandler;
      const wrappedRequest = DrmLicenseHelper.wrapLicenseRequest(
        challenge,
        castToken
      );
      requestInfo.content = wrappedRequest;
      return requestInfo;
    };
    playbackConfig.licenseHandler = license => {
      const unwrappedLicense = DrmLicenseHelper.unwrapLicenseResponse(license);
      return unwrappedLicense;
    };

    // Duration of buffered media in seconds to start/resume playback after auto-paused due to buffering; default is 10.
    playbackConfig.autoResumeDuration = 4;

    // Minimum number of buffered segments to start/resume playback.
    playbackConfig.initialBandwidth = 1200000;

    context.start({
      touchScreenOptimizedApp: true,
      playbackConfig: playbackConfig,
      supportedCommands: cast.framework.messages.Command.ALL_BASIC_MEDIA
    });
  }
}

LoadHandler 可选地添加一个代理(我正在使用 cors-anywhere 代理来删除原始标头),并为 licenseRequests 存储 castToken:

class LoadHandler {
  CORS_USE_PROXY = true;
  CORS_PROXY = "http://192.168.0.127:8003";

  castToken = null;

  onLoadMedia(loadRequestData, playerManager) {
    if (!loadRequestData) {
      return;
    }
    const { media } = loadRequestData;

    // disable cors for local testing
    if (this.CORS_USE_PROXY) {
      media.contentId = `${this.CORS_PROXY}/${media.contentId}`;
    }

    const { customData } = media;
    if (customData) {
      const { licenseUrl, castToken } = customData;

      // install cast token
      this.castToken = castToken;

      // handle license URL
      if (licenseUrl) {
        const playbackConfig = playerManager.getPlaybackConfig();
        playbackConfig.licenseUrl = licenseUrl;
        const { contentType } = loadRequestData.media;

        // Dash: "application/dash+xml"
        playbackConfig.protectionSystem = cast.framework.ContentProtection.WIDEVINE;

        // disable cors for local testing
        if (this.CORS_USE_PROXY) {
          playbackConfig.licenseUrl = `${this.CORS_PROXY}/${licenseUrl}`;
        }
      }
    }
  }
}

DrmHelper 包装许可证请求以添加 castToken 和 base64 编码整个。许可证响应经过 base64 解码并稍后解包:

export default class DrmLicenseHelper {
  static wrapLicenseRequest(challenge, castToken) {
    const wrapped = {};
    wrapped.AuthToken = castToken;
    wrapped.Payload = fromByteArray(new Uint8Array(challenge));
    const wrappedJson = JSON.stringify(wrapped);
    const wrappedLicenseRequest = fromByteArray(
      new TextEncoder().encode(wrappedJson)
    );
    return wrappedLicenseRequest;
  }

  static unwrapLicenseResponse(license) {
    try {
      const responseString = String.fromCharCode.apply(String, license);
      const responseJson = JSON.parse(responseString);
      const rawLicenseBase64 = responseJson.license;
      const decodedLicense = toByteArray(rawLicenseBase64);
      return decodedLicense;
    } catch (e) {
      return license;
    }
  }
}

【问题讨论】:

  • 你好。我有类似的问题,但没有修复喷气机。您能回答这个问题吗:您使用的是什么设备?什么版本的谷歌演员?什么媒体格式?您在定义广告插播时间吗?
  • 我主要投射到内置 chromecast 的 Android 电视盒。这个问题似乎不是特定于设备的。 Chromecast 版本目前为 1.36.168363。流媒体协议是 DASH(和 Widevine DRM)。没有广告插播时间。
  • 今天我们在删除 cookie 以解锁设备方面同样幸运。明天我们将彻底检查这个和关于 LOAD 处理程序的建议。
  • 谢谢。所以会话被保存并从 cookie 中恢复?
  • LoadHandler 是什么?

标签: chromecast google-cast


【解决方案1】:

cast.framework.messages.MessageType.LOAD 的处理程序应始终返回:

  • (可能已修改)loadRequestData,或
  • 对(可能已修改)loadRequestData 的承诺
  • null 丢弃加载请求(我不能 100% 确定这适用于加载请求)

如果您不这样做,加载请求将保留在队列中,并且任何新请求都会在初始请求之后排队。

在您的处理程序中,如果!loadRequestData.media,您将返回一个错误,这将使您进入该状态。另一种可能性是加载请求处理程序中的异常,它也会使您进入该状态。

【讨论】:

  • 谢谢。除此之外,有没有办法以干净的状态开始演员会议?那么为什么在结束“卡住”会话后保持这种状态?
  • 我认为不可能从接收器重新启动投射会话以进入干净状态。根据我的经验,CAF 在处理截获请求的方式上不是很灵活。特别是处理 LOAD 请求可能会很痛苦,特别是如果您需要异步执行,并且需要支持在前一个尚未完成时处理新的 LOAD 请求。这可能是另一种触发“正在加载”的场景......
【解决方案2】:

我想我们有不同的方法,并通过sendMessage 发送所有可能的内容,当我们加载内容时,我们创建一个new cast.framework.messages.LoadRequestData(),我们使用playerManager.load(loadRequest) 加载它。

但我猜您可能正在集成 Chromecast 上进行测试,我们也看到了这个问题!?

我建议你做一个或多个

  • 对所有响应启用 gzip 压缩!!!
  • 停止播放 playerManager.stop()(可能在 interseptor 中?)
  • 更改licenseUrl 的设置方式

我们如何设置licenseUrl

playerManager.setMediaPlaybackInfoHandler((loadRequestData, playbackConfig) => {
    playbackConfig.licenseUrl = loadRequestData.customData.licenseUrl;
    return playbackConfig;
  }
);

【讨论】:

    猜你喜欢
    • 2015-05-28
    • 2021-12-10
    • 2020-04-04
    • 1970-01-01
    • 2015-01-10
    • 2015-03-06
    • 2023-03-24
    • 2014-03-10
    • 1970-01-01
    相关资源
    最近更新 更多