Мое собственное приложение ресивера v3 CAF успешно воспроизводит первые несколько живых и видео активов. После этого он переходит в состояние, когда команды мультимедиа ставятся в очередь из-за «Идет загрузка». Это все еще (успешно) извлекает манифесты, но MEDIA_STATUS остается "буферизировать". Журнал затем показывает:
[4.537s] [cast.receiver.MediaManager] Загрузка выполняется, команда мультимедиа находится в очереди.
[5.893s] [cast.receiver.MediaManager] Состояние буферизации изменено, isPlayerBuffering: истина, старое время: 0 текущее время: 0
[5.897s] [cast.receiver.MediaManager] Отправка сообщения о состоянии широковещания
Событие ядра CastContext: {"Тип": "MEDIA_STATUS", "mediaStatus": { "mediaSessionId": 1, "playbackRate": 1, "playerState": "БУФЕРИЗАЦИЯ", "CURRENTTIME": 0, "supportedMediaCommands": 12303, "объем": {"уровень": 1, "приглушен" ложь}, "currentItemId": 1, "repeatMode": "REPEAT_OFF", "liveSeekableRange": { "старт": 0, "конец": +20,000999927520752, "isMovingWindow": правда,"isLiveDone": false}}}
Событие CastContext MEDIA_STATUS: {"type": "MEDIA_STATUS", "mediaStatus": {"mediaSessionId": 1, «PlayRate": 1, «playerState»: «BUFFERING»», "CURRENTTIME": 0, "supportedMediaCommands": 12303, "объем": { "уровень": 1, "приглушен" ложь}, "currentItemId": 1, "repeatMode": "REPEAT_OFF", "liveSeekableRange":{ "СТОrt ": 0," end ": 20.000999927520752," isMovingWindow ": true," isLiveDone ": false}}}
Получить законченную загрузку: GET" (url манифеста) ".
Ошибки не отображаются.
Даже после закрытия и перезапуска сеанса приведения проблема остается. Само устройство приведения должно быть перезагружено для его разрешения. Похоже, что данные хранятся между сессиями.
Важно отметить, что приложение для приёмника еще не опубликовано. Он размещен в локальной сети.
Мои вопросы:
- В чем может быть причина этого зависшего поведения?
- Есть ли какие-либо данные сеанса междусеанс?
- Как полностью сбросить приложение приёмника, без необходимости перезапуска устройства приведения.
Само приложение приёмника очень простое. В отличие от переноса лицензии, он напоминает пример приложения vanilla:
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 -where для удаления заголовка источника) и сохраняет castToken для licenseRequests:
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;
}
}
}