Skip to content

Commit

Permalink
Fix: [Client][Player/PlayerController] ビデオ視聴時の keep-alive がバックグランドタブに…
Browse files Browse the repository at this point in the history
…回ると途切れてしまう問題を修正

かなり邪悪なハックを見つけたので早速活用させていただいた
  • Loading branch information
tsukumijima committed Nov 24, 2023
1 parent b235b73 commit 514ecc2
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 10 deletions.
24 changes: 14 additions & 10 deletions client/src/services/player/PlayerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,11 @@ class PlayerController {
// 設計上コンストラクタ以降で変更すべきでないため readonly にしている
private readonly live_playback_buffer_seconds: number;

// ライブ視聴: mpegts.js のバッファ詰まり対策で定期的に強制シークするインターバルのタイマー ID
// 保持しておかないと clearInterval() でタイマーを止められない
private live_force_seek_interval_timer_id: number = 0;
// ライブ視聴: mpegts.js のバッファ詰まり対策で定期的に強制シークするインターバルをキャンセルする関数
private live_force_seek_interval_timer_cancel: (() => void) | null = null;

// ビデオ視聴: ビデオストリームのアクティブ状態を維持するために Keep-Alive API にリクエストを送るインターバルのタイマー ID
// 保持しておかないと clearInterval() でタイマーを止められない
private video_keep_alive_interval_timer_id: number = 0;
// ビデオ視聴: ビデオストリームのアクティブ状態を維持するために Keep-Alive API にリクエストを送るインターバルのキャンセルする関数
private video_keep_alive_interval_timer_cancel: (() => void) | null = null;

// setupPlayerContainerResizeHandler() で利用する ResizeObserver
// 保持しておかないと disconnect() で ResizeObserver を止められない
Expand Down Expand Up @@ -754,7 +752,7 @@ class PlayerController {
// mpegts.js の仕様上、MSE 側に未再生のバッファが貯まり過ぎると新規に SourceBuffer が追加できなくなるため、強制的に接続が切断されてしまう
// 再生停止状態でも定期的にシークすることで、バッファが貯まりすぎないように調節する
if (this.playback_mode === 'Live') {
this.live_force_seek_interval_timer_id = window.setInterval(() => {
this.live_force_seek_interval_timer_cancel = Utils.setIntervalInWorker(() => {
if (this.player === null) return;
if ((this.player.video.paused && this.player.video.buffered.length >= 1) &&
(this.player.video.buffered.end(0) - this.player.video.currentTime > 30)) {
Expand All @@ -768,7 +766,7 @@ class PlayerController {
// それだけではタイミング次第では十分ではないため、定期的に Keep-Alive を行う
// Keep-Alive が行われなくなったタイミングで、サーバー側で自動的にビデオストリームの終了処理 (エンコードタスクの停止) が行われる
if (this.playback_mode === 'Video') {
this.video_keep_alive_interval_timer_id = window.setInterval(async () => {
this.video_keep_alive_interval_timer_cancel = Utils.setIntervalInWorker(async () => {
// 画質切り替えでベース URL が変わることも想定し、あえて毎回 API URL を取得している
if (this.player === null) return;
const api_quality = PlayerUtils.extractVideoAPIQualityFromDPlayer(this.player);
Expand Down Expand Up @@ -1358,8 +1356,14 @@ class PlayerController {
}

// タイマーを破棄
window.clearInterval(this.live_force_seek_interval_timer_id);
window.clearInterval(this.video_keep_alive_interval_timer_id);
if (this.live_force_seek_interval_timer_cancel !== null) {
this.live_force_seek_interval_timer_cancel();
this.live_force_seek_interval_timer_cancel = null;
}
if (this.video_keep_alive_interval_timer_cancel !== null) {
this.video_keep_alive_interval_timer_cancel();
this.video_keep_alive_interval_timer_cancel = null;
}
window.clearTimeout(this.player_control_ui_hide_timer_id);

// プレイヤー全体のコンテナ要素の監視を停止
Expand Down
29 changes: 29 additions & 0 deletions client/src/utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,35 @@ export default class Utils {
}


/**
* setInterval() を Web Worker で実行する
* 実際にコールバック関数が実行される環境自体はメインスレッドで、タブがバックグラウンドに回った場合でも Web Worker 上でコールバックが叩き起こされる
* 通常タブがバックグラウンドに回ると setInterval() が勝手に間引かれてしまうので、その影響を受けない Web Worker から叩き起こすことで間引きを防ぐ (邪悪…)
* ref: https://gist.github.com/kawaz/72f61d8389fed0e9d4e7dc9eb01b39c8
* @param callback コールバック関数
* @param interval インターバル (ミリ秒単位)
* @param args コールバック関数に渡す引数 (可変長)
* @returns setInterval を停止するための関数 (戻り値が本家 setInterval() と異なるので注意)
*/
static setIntervalInWorker(callback: (...args: any[]) => void, interval: number = 1000, ...args: any[]): () => void {
try {
const worker_code = `
self.addEventListener('message', msg => {
setInterval(() => self.postMessage(null), msg.data);
});
`;
const worker = new Worker(`data:text/javascript;base64,${btoa(worker_code)}`);
worker.onmessage = () => callback(...args); // Web Worker 側から叩き起こしてコールバック関数を実行する
worker.postMessage(interval);
return () => worker.terminate();
} catch (_) {
// なんらかの理由で Web Worker が使えなければ通常の setInterval() を使う
const timer_id = setInterval(callback, interval, ...args);
return () => clearInterval(timer_id);
}
}


/**
* async/await でスリープ的なもの
* @param seconds 待機する秒数 (ミリ秒単位ではないので注意)
Expand Down

0 comments on commit 514ecc2

Please sign in to comment.