Skip to content

Instantly share code, notes, and snippets.

@kocoten1992
Last active May 30, 2021 01:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kocoten1992/97ce71a733b824f32768f4d84f53a6fe to your computer and use it in GitHub Desktop.
Save kocoten1992/97ce71a733b824f32768f4d84f53a6fe to your computer and use it in GitHub Desktop.
canvas_text_displayer plugin
/*! @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);
}
};
.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