Last active
May 30, 2021 01:20
-
-
Save kocoten1992/97ce71a733b824f32768f4d84f53a6fe to your computer and use it in GitHub Desktop.
canvas_text_displayer plugin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! @license | |
* Shaka Player | |
* Copyright 2016 Google LLC | |
* SPDX-License-Identifier: Apache-2.0 | |
*/ | |
goog.provide('shaka.text.CanvasTextDisplayer'); | |
goog.require('goog.asserts'); | |
goog.require('shaka.text.Cue'); | |
goog.require('shaka.util.Dom'); | |
goog.require('shaka.util.EventManager'); | |
goog.require('shaka.util.Timer'); | |
/** | |
* The text displayer plugin for the Shaka Player UI. Can also be used directly | |
* by providing an appropriate container element. | |
* | |
* @implements {shaka.extern.TextDisplayer} | |
* @final | |
* @export | |
*/ | |
shaka.text.CanvasTextDisplayer = class { | |
/** | |
* Constructor. | |
* @param {HTMLMediaElement} video | |
* @param {HTMLElement} videoContainer | |
*/ | |
constructor(video, videoContainer) { | |
goog.asserts.assert(videoContainer, 'videoContainer should be valid.'); | |
/** @private {boolean} */ | |
this.isTextVisible_ = false; | |
/** @private {!Set.<!shaka.text.Cue>} */ | |
this.cues_ = new Set; | |
/** @private {HTMLMediaElement} */ | |
this.video_ = video; | |
/** @private {HTMLElement} */ | |
this.videoContainer_ = videoContainer; | |
/** @type {HTMLCanvasElement} */ | |
this.canvasContainer_ = shaka.util.Dom.createHTMLCanvasElement(); | |
this.canvasContainer_.classList.add('shaka-text-canvas-container'); | |
/** @type {!CanvasRenderingContext2D} */ | |
this.canvasContext_ = /** @type {!CanvasRenderingContext2D} */ (this.canvasContainer_.getContext('2d')); | |
this.videoContainer_.appendChild(this.canvasContainer_); | |
/** | |
* The captions' update period in seconds. | |
* @private {number} | |
*/ | |
const updatePeriod = 0.02; // 20ms | |
/** @private {shaka.util.Timer} */ | |
this.captionsTimer_ = new shaka.util.Timer(() => { | |
this.updateCaptions_(); | |
}).tickEvery(updatePeriod); | |
/** private {Map.<!shaka.extern.Cue, !HTMLElement>} */ | |
this.currentCuesMap_ = new Set; | |
/** @private {shaka.util.EventManager} */ | |
this.eventManager_ = new shaka.util.EventManager(); | |
this.eventManager_.listen(document, 'fullscreenchange', () => { | |
this.updateCaptions_(/* forceUpdate= */ true); | |
}); | |
} | |
/** | |
* @override | |
* @export | |
*/ | |
append(cues) { | |
// Clone the cues list for performace optimization. We can avoid the cues | |
// list growing during the comparisons for duplicate cues. | |
// See: https://github.com/google/shaka-player/issues/3018 | |
const cuesList = [...this.cues_]; | |
for (const cue of cues) { | |
// When a VTT cue spans a segment boundary, the cue will be duplicated | |
// into two segments. | |
// To avoid displaying duplicate cues, if the current cue list already | |
// contains the cue, skip it. | |
const containsCue = cuesList.some( | |
(cueInList) => shaka.text.Cue.equal(cueInList, cue)); | |
if (!containsCue) { | |
this.cues_.add(cue); | |
} | |
} | |
this.updateCaptions_(); | |
} | |
/** | |
* @override | |
* @export | |
*/ | |
destroy() { | |
// Remove the text container element from the UI. | |
this.videoContainer_.removeChild(this.canvasContainer_); | |
this.canvasContainer_ = null; | |
this.isTextVisible_ = false; | |
this.cues_ = new Set; | |
if (this.captionsTimer_) { | |
this.captionsTimer_.stop(); | |
} | |
this.currentCuesMap_.clear(); | |
// Tear-down the event manager to ensure messages stop moving around. | |
if (this.eventManager_) { | |
this.eventManager_.release(); | |
this.eventManager_ = null; | |
} | |
} | |
/** | |
* @override | |
* @export | |
*/ | |
remove(start, end) { | |
// Return false if destroy() has been called. | |
if (!this.canvasContainer_) { | |
return false; | |
} | |
// Remove the cues out of the time range. | |
this.cues_.forEach(cue => { | |
if (cue.startTime > start && cue.endTime <= end) { | |
this.cues_.delete(cue); | |
} | |
}); | |
this.updateCaptions_(); | |
return true; | |
} | |
/** | |
* @override | |
* @export | |
*/ | |
isTextVisible() { | |
return this.isTextVisible_; | |
} | |
/** | |
* @override | |
* @export | |
*/ | |
setTextVisibility(on) { | |
this.isTextVisible_ = on; | |
} | |
/** | |
* Determine cue should be display | |
* @param {!shaka.text.Cue} cue | |
* @return {boolean} | |
*/ | |
shouldCueBeDisplayed_(cue) { | |
const currentTime = this.video_.currentTime; | |
// Return true if the cue should be displayed at the current time point. | |
return this.cues_.has(cue) && this.isTextVisible_ && | |
cue.startTime <= currentTime && cue.endTime > currentTime; | |
} | |
/** | |
* Dynamic resize canvas | |
* (in case user zoom in/out or fullscreen) | |
* @private | |
*/ | |
resizeCanvas_() { | |
this.canvasContainer_.width = this.videoContainer_.offsetWidth; | |
this.canvasContainer_.height = this.videoContainer_.offsetHeight; | |
} | |
/** | |
* Display the current captions. | |
* @param {boolean=} forceUpdate | |
* @private | |
*/ | |
updateCaptions_(forceUpdate = false) { | |
// remove cues when end time has passed | |
for (const cue of this.currentCuesMap_.keys()) { | |
if (!this.shouldCueBeDisplayed_(cue) || forceUpdate) { | |
this.clearCanvas_(); | |
this.currentCuesMap_.delete(cue); | |
this.resizeCanvas_(); | |
} | |
} | |
// add cues when start time passed and end time not reach yet | |
this.cues_.forEach(cue => { | |
if (this.shouldCueBeDisplayed_(cue) && !this.currentCuesMap_.has(cue)) { | |
this.resizeCanvas_(); | |
this.displayCue_(cue, /* isNested */ false); | |
this.currentCuesMap_.add(cue); | |
} | |
}); | |
} | |
/** | |
* Displays a cue | |
* | |
* @param {!shaka.extern.Cue} cue | |
* @param {boolean} isNested | |
* @private | |
*/ | |
displayCue_(cue, isNested) { | |
if (isNested) { | |
for (const nestedCue of cue.nestedCues) { | |
this.displayCue_(nestedCue, /* isNested= */ true); | |
return; | |
} | |
} | |
// spit cue.payload if contain split line character | |
const lines = cue.payload.split(/\r\n|\r|\n/); | |
for (var i = 0; i < lines.length; i++) { | |
var fontSize = 11 + (1.5 * this.videoContainer_.clientWidth / 100); | |
var fontFamily = 'Roboto'; | |
var fontWeight = '700'; | |
var fontLineHeight = 1.5; | |
var fixedBottom = 36; | |
var elasticBottom = 4.4/100; // 4.4% | |
var height = this.videoContainer_.offsetHeight | |
- fixedBottom | |
- (this.videoContainer_.offsetHeight * elasticBottom) | |
- ((lines.length - i) * fontSize * fontLineHeight); | |
for (var y = 0; y < 4; y++) { | |
this.canvasContext_.font = '' + fontWeight + ' ' + fontSize + 'px ' + fontFamily; | |
this.canvasContext_.textAlign = 'center'; | |
this.canvasContext_.fillStyle = 'white'; | |
// we want to achieve special font effect (multiple text shadow) | |
// for example: '4px 4px 0 #000, -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000' | |
if (y == 0) { | |
this.canvasContext_.shadowOffsetX = 4; | |
this.canvasContext_.shadowOffsetY = 4; | |
this.canvasContext_.shadowColor = "black"; | |
this.canvasContext_.shadowBlur = 0; | |
} else if (y == 1) { | |
this.canvasContext_.shadowOffsetX = -2; | |
this.canvasContext_.shadowOffsetY = -2; | |
this.canvasContext_.shadowColor = "black"; | |
this.canvasContext_.shadowBlur = 0; | |
} else if (y == 2) { | |
this.canvasContext_.shadowOffsetX = -2; | |
this.canvasContext_.shadowOffsetY = 2; | |
this.canvasContext_.shadowColor = "black"; | |
this.canvasContext_.shadowBlur = 0; | |
} else if (y == 3) { | |
this.canvasContext_.shadowOffsetX = 2; | |
this.canvasContext_.shadowOffsetY = 2; | |
this.canvasContext_.shadowColor = "black"; | |
this.canvasContext_.shadowBlur = 0; | |
} | |
this.canvasContext_.fillText(lines[i], this.canvasContainer_.width/2, height); | |
} | |
} | |
} | |
/** | |
* Clear canvas | |
* @private | |
*/ | |
clearCanvas_() { | |
this.canvasContext_.clearRect(0, 0, this.canvasContainer_.width, this.canvasContainer_.height); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.shaka-text-canvas-container { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment