mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
feat: Add Watermark Support to Shaka Player UI (#7877)
This pull request introduces watermark support for the Shaka Player UI. Added functionality to overlay static or dynamic watermarks on videos. Included options to customize position, size, color, and opacity. Enhanced content security with dynamic watermarking capabilities. Resolves #7726 .
This commit is contained in:
@@ -164,3 +164,4 @@ Koen Romers <koenromers@gmail.com>
|
||||
Zhenghang Chen <czhtju@gmail.com>
|
||||
Ashley Manners <ashley.manners@xperi.com>
|
||||
Bidisha Das <officialbidisha1@gmail.com>
|
||||
Chafroud Tarek <chafroudtarek3@gmail.com>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
+../../ui/ad_statistics_button.js
|
||||
+../../ui/audio_language_selection.js
|
||||
+../../ui/externs/ui.js
|
||||
+../../ui/externs/watermark.js
|
||||
+../../ui/play_button.js
|
||||
+../../ui/big_play_button.js
|
||||
+../../ui/airplay_button.js
|
||||
@@ -50,5 +51,6 @@
|
||||
+../../ui/vr_utils.js
|
||||
+../../ui/vr_webgl.js
|
||||
|
||||
+../../ui/watermark.js
|
||||
+../../ui/gl_matrix/matrix_4x4.js
|
||||
+../../ui/gl_matrix/matrix_quaternion.js
|
||||
|
||||
@@ -767,6 +767,11 @@ shakaDemo.Config = class {
|
||||
this.latestInput_.input().checked = true;
|
||||
}
|
||||
|
||||
this.addCustomTextInput_('Watermark text', (input) => {
|
||||
shakaDemoMain.setWatermarkText(input.value);
|
||||
});
|
||||
this.latestInput_.input().value = shakaDemoMain.getWatermarkText();
|
||||
|
||||
// shaka.log is not set if logging isn't enabled.
|
||||
// I.E. if using the release version of shaka.
|
||||
if (!shaka['log']) {
|
||||
|
||||
@@ -62,6 +62,9 @@ shakaDemo.Main = class {
|
||||
/** @private {boolean} */
|
||||
this.customContextMenu_ = false;
|
||||
|
||||
/** @private {string} */
|
||||
this.watermarkText_ = '';
|
||||
|
||||
/** @private {boolean} */
|
||||
this.nativeControlsEnabled_ = false;
|
||||
|
||||
@@ -397,6 +400,11 @@ shakaDemo.Main = class {
|
||||
uiConfig.overflowMenuButtons.push('visualizer');
|
||||
}
|
||||
ui.configure(uiConfig);
|
||||
if (this.watermarkText_) {
|
||||
ui.setTextWatermark(this.watermarkText_);
|
||||
} else {
|
||||
ui.removeWatermark();
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
@@ -829,6 +837,27 @@ shakaDemo.Main = class {
|
||||
return this.customContextMenu_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the text for watermark.
|
||||
*
|
||||
* @param {string} text
|
||||
*/
|
||||
setWatermarkText(text) {
|
||||
this.watermarkText_ = text;
|
||||
// Configure the UI, to add or remove the controls.
|
||||
this.configureUI_();
|
||||
this.remakeHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current text for watermark.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
getWatermarkText() {
|
||||
return this.watermarkText_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the native controls.
|
||||
* Goes into effect during the next load.
|
||||
@@ -1020,6 +1049,10 @@ shakaDemo.Main = class {
|
||||
this.configureUI_();
|
||||
}
|
||||
|
||||
if ('watermarkText' in params) {
|
||||
this.watermarkText_ = params['watermarkText'];
|
||||
}
|
||||
|
||||
if ('visualizer' in params) {
|
||||
this.setIsVisualizerActive(true);
|
||||
} else {
|
||||
@@ -1601,6 +1634,10 @@ shakaDemo.Main = class {
|
||||
params.push('customContextMenu');
|
||||
}
|
||||
|
||||
if (this.watermarkText_) {
|
||||
params.push('watermarkText=' + this.watermarkText_);
|
||||
}
|
||||
|
||||
if (this.getIsVisualizerActive()) {
|
||||
params.push('visualizer');
|
||||
}
|
||||
@@ -2038,3 +2075,4 @@ document.addEventListener('shaka-ui-load-failed', (event) => {
|
||||
shakaDemoMain.initFailed(reasonCode);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -106,6 +106,19 @@ shaka.test.FakeDemoMain = class {
|
||||
this.getAssetUnsupportedReason =
|
||||
jasmine.createSpy('getAssetUnsupportedReason');
|
||||
this.getAssetUnsupportedReason.and.returnValue(null);
|
||||
|
||||
/** @private {string} */
|
||||
this.watermarkText_ = '';
|
||||
|
||||
/** @type {!jasmine.Spy} */
|
||||
this.getWatermarkText = jasmine.createSpy('getWatermarkText');
|
||||
this.getWatermarkText.and.callFake(() => this.watermarkText_);
|
||||
|
||||
/** @type {!jasmine.Spy} */
|
||||
this.setWatermarkText = jasmine.createSpy('setWatermarkText');
|
||||
this.setWatermarkText.and.callFake((text) => {
|
||||
this.watermarkText_ = text;
|
||||
});
|
||||
}
|
||||
|
||||
/** Creates and assigns the mock demo main (and all of the real tab). */
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*! @license
|
||||
* Shaka Player
|
||||
* Copyright 2016 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @externs
|
||||
* @suppress {duplicate} To prevent compiler errors with the namespace
|
||||
* being declared both here and by goog.provide in the library.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: string,
|
||||
* text: string,
|
||||
* position: string,
|
||||
* color: string,
|
||||
* size: number,
|
||||
* alpha: number,
|
||||
* interval: number,
|
||||
* skip: number,
|
||||
* displayDuration: number,
|
||||
* transitionDuration: number
|
||||
* }}
|
||||
*
|
||||
* @property {string} type
|
||||
* The type of watermark ('static' or 'dynamic').
|
||||
* Defaults to 'static'.
|
||||
* @property {string} text
|
||||
* The text content of the watermark. Required.
|
||||
* @property {string} position
|
||||
* Position of the watermark.
|
||||
* Defaults to 'top-left'.
|
||||
* @property {string} color
|
||||
* The color of the watermark text.
|
||||
* Defaults to 'white'.
|
||||
* @property {number} size
|
||||
* Font size of the watermark text in pixels.
|
||||
* Defaults to 24.
|
||||
* @property {number} alpha
|
||||
* Opacity of the watermark (0.0 to 1.0).
|
||||
* Defaults to 0.7.
|
||||
* @property {number} interval
|
||||
* Interval between position updates for dynamic watermarks (in seconds).
|
||||
* Only used when type is 'dynamic'.
|
||||
* Defaults to 2.
|
||||
* @property {number} skip
|
||||
* Skip duration for dynamic watermarks (in seconds).
|
||||
* Only used when type is 'dynamic'.
|
||||
* Defaults to 0.5.
|
||||
* @property {number} displayDuration
|
||||
* Duration to display watermark at each position (in seconds).
|
||||
* Only used when type is 'dynamic'.
|
||||
* Defaults to 2.
|
||||
* @property {number} transitionDuration
|
||||
* Duration of fade transitions between positions (in seconds).
|
||||
* Only used when type is 'dynamic'.
|
||||
* Defaults to 0.5.
|
||||
* @exportDoc
|
||||
*/
|
||||
shaka.ui.Watermark.Options;
|
||||
@@ -14,6 +14,7 @@ goog.require('shaka.Player');
|
||||
goog.require('shaka.log');
|
||||
goog.require('shaka.polyfill');
|
||||
goog.require('shaka.ui.Controls');
|
||||
goog.require('shaka.ui.Watermark');
|
||||
goog.require('shaka.util.ConfigUtils');
|
||||
goog.require('shaka.util.Dom');
|
||||
goog.require('shaka.util.FakeEvent');
|
||||
@@ -66,6 +67,11 @@ shaka.ui.Overlay = class {
|
||||
|
||||
videoContainer['ui'] = this;
|
||||
video['ui'] = this;
|
||||
/** @private {shaka.ui.Watermark} */
|
||||
this.watermark_ = new shaka.ui.Watermark(
|
||||
this.videoContainer_,
|
||||
this.controls_,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +89,7 @@ shaka.ui.Overlay = class {
|
||||
await this.player_.destroy();
|
||||
}
|
||||
this.player_ = null;
|
||||
this.watermark_ = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +175,27 @@ shaka.ui.Overlay = class {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {?shaka.ui.Watermark.Options=} options
|
||||
* @export
|
||||
*/
|
||||
setTextWatermark(text, options) {
|
||||
if (this.watermark_) {
|
||||
this.watermark_.setTextWatermark(text, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @export
|
||||
*/
|
||||
removeWatermark() {
|
||||
if (this.watermark_) {
|
||||
this.watermark_.removeWatermark();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return {!shaka.extern.UIConfiguration}
|
||||
* @private
|
||||
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
/*! @license
|
||||
* Shaka Player
|
||||
* Copyright 2016 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
goog.provide('shaka.ui.Watermark');
|
||||
|
||||
goog.requireType('shaka.ui.Controls');
|
||||
|
||||
goog.require('shaka.ui.Element');
|
||||
goog.require('shaka.log');
|
||||
|
||||
/**
|
||||
* A UI component that adds watermark functionality to the Shaka Player.
|
||||
* Allows adding text watermarks with various customization options.
|
||||
* @extends {shaka.ui.Element}
|
||||
* @final
|
||||
* @export
|
||||
*/
|
||||
shaka.ui.Watermark = class extends shaka.ui.Element {
|
||||
/**
|
||||
* Creates a new Watermark instance.
|
||||
* @param {!HTMLElement} parent The parent element for the watermark canvas
|
||||
* @param {!shaka.ui.Controls} controls The controls instance
|
||||
*/
|
||||
constructor(parent, controls) {
|
||||
super(parent, controls);
|
||||
|
||||
/** @private {!HTMLCanvasElement} */
|
||||
this.canvas_ = /** @type {!HTMLCanvasElement} */ (
|
||||
document.createElement('canvas')
|
||||
);
|
||||
this.canvas_.style.position = 'absolute';
|
||||
this.canvas_.style.top = '0';
|
||||
this.canvas_.style.left = '0';
|
||||
this.canvas_.style.pointerEvents = 'none';
|
||||
|
||||
this.parent.appendChild(this.canvas_);
|
||||
this.resizeCanvas_();
|
||||
|
||||
/** @private {number|null} */
|
||||
this.animationId_ = null;
|
||||
|
||||
/** @private {ResizeObserver|null} */
|
||||
this.resizeObserver_ = null;
|
||||
|
||||
// Use ResizeObserver if available, fallback to window resize event
|
||||
if (window.ResizeObserver) {
|
||||
this.resizeObserver_ = new ResizeObserver(() => this.resizeCanvas_());
|
||||
this.resizeObserver_.observe(this.parent);
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
window.addEventListener('resize', () => this.resizeCanvas_());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 2D rendering context safely
|
||||
* @return {?CanvasRenderingContext2D}
|
||||
* @private
|
||||
*/
|
||||
getContext2D_() {
|
||||
const ctx = this.canvas_.getContext('2d');
|
||||
if (!ctx) {
|
||||
shaka.log.error('2D context is not available');
|
||||
return null;
|
||||
}
|
||||
return /** @type {!CanvasRenderingContext2D} */ (ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize canvas to match video container
|
||||
* @private
|
||||
*/
|
||||
resizeCanvas_() {
|
||||
this.canvas_.width = this.parent.offsetWidth;
|
||||
this.canvas_.height = this.parent.offsetHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a text watermark on the video with customizable options.
|
||||
* The watermark can be either static (fixed position) or dynamic (moving).
|
||||
* @param {string} text The text to display as watermark
|
||||
* @param {?shaka.ui.Watermark.Options=} options configuration options
|
||||
* @export
|
||||
*/
|
||||
setTextWatermark(text, options) {
|
||||
/** @type {!shaka.ui.Watermark.Options} */
|
||||
const defaultOptions = {
|
||||
type: 'static',
|
||||
text: text,
|
||||
position: 'top-right',
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
size: 20,
|
||||
alpha: 0.7,
|
||||
interval: 2 * 1000,
|
||||
skip: 0.5 * 1000,
|
||||
displayDuration: 2 * 1000,
|
||||
transitionDuration: 0.5,
|
||||
};
|
||||
|
||||
/** @type {!shaka.ui.Watermark.Options} */
|
||||
const config = /** @type {!shaka.ui.Watermark.Options} */ (
|
||||
Object.assign({}, defaultOptions, options || defaultOptions)
|
||||
);
|
||||
|
||||
if (config.type === 'static') {
|
||||
this.drawStaticWatermark_(config);
|
||||
} else if (config.type === 'dynamic') {
|
||||
this.startDynamicWatermark_(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a static watermark on the canvas.
|
||||
* @param {!shaka.ui.Watermark.Options} config configuration options
|
||||
* @private
|
||||
*/
|
||||
drawStaticWatermark_(config) {
|
||||
const ctx = this.getContext2D_();
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
|
||||
|
||||
ctx.globalAlpha = config.alpha;
|
||||
ctx.fillStyle = config.color;
|
||||
ctx.font = `${config.size}px Arial`;
|
||||
|
||||
const metrics = ctx.measureText(config.text);
|
||||
const padding = 20;
|
||||
let x;
|
||||
let y;
|
||||
|
||||
switch (config.position) {
|
||||
case 'top-left':
|
||||
x = padding;
|
||||
y = config.size + padding;
|
||||
break;
|
||||
case 'top-right':
|
||||
x = this.canvas_.width - metrics.width - padding;
|
||||
y = config.size + padding;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x = padding;
|
||||
y = this.canvas_.height - padding;
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x = this.canvas_.width - metrics.width - padding;
|
||||
y = this.canvas_.height - padding;
|
||||
break;
|
||||
default:
|
||||
x = (this.canvas_.width - metrics.width) / 2;
|
||||
y = (this.canvas_.height + config.size) / 2;
|
||||
}
|
||||
|
||||
ctx.fillText(config.text, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a dynamic watermark animation on the canvas.
|
||||
* @param {!shaka.ui.Watermark.Options} config configuration options
|
||||
* @private
|
||||
*/
|
||||
startDynamicWatermark_(config) {
|
||||
const ctx = /** @type {!CanvasRenderingContext2D} */ (
|
||||
this.canvas_.getContext('2d')
|
||||
);
|
||||
let currentPosition = {left: 0, top: 0};
|
||||
let currentAlpha = 0;
|
||||
let phase = 'fadeIn'; // States: fadeIn, display, fadeOut, transition
|
||||
|
||||
let displayFrames = Math.round(config.displayDuration * 60); // 60fps
|
||||
const transitionFrames = Math.round(config.transitionDuration * 60);
|
||||
const fadeSpeed = 1 / (transitionFrames / 2); // Smoother fade speed
|
||||
|
||||
/** @private {number} */
|
||||
let positionIndex = 0;
|
||||
|
||||
const getNextPosition = () => {
|
||||
ctx.font = `${config.size}px Arial`;
|
||||
const textMetrics = ctx.measureText(config.text);
|
||||
const textWidth = textMetrics.width;
|
||||
const textHeight = config.size;
|
||||
const padding = 20;
|
||||
|
||||
// Define fixed positions
|
||||
const positions = [
|
||||
// Top-left
|
||||
{
|
||||
left: padding,
|
||||
top: textHeight + padding,
|
||||
},
|
||||
// Top-right
|
||||
{
|
||||
left: this.canvas_.width - textWidth - padding,
|
||||
top: textHeight + padding,
|
||||
},
|
||||
// Bottom-left
|
||||
{
|
||||
left: padding,
|
||||
top: this.canvas_.height - padding,
|
||||
},
|
||||
// Bottom-right
|
||||
{
|
||||
left: this.canvas_.width - textWidth - padding,
|
||||
top: this.canvas_.height - padding,
|
||||
},
|
||||
// Center
|
||||
{
|
||||
left: (this.canvas_.width - textWidth) / 2,
|
||||
top: (this.canvas_.height + textHeight) / 2,
|
||||
},
|
||||
];
|
||||
|
||||
// Cycle through positions
|
||||
const position = positions[positionIndex];
|
||||
positionIndex = (positionIndex + 1) % positions.length;
|
||||
return position;
|
||||
};
|
||||
|
||||
currentPosition = getNextPosition();
|
||||
|
||||
const updateWatermark = () => {
|
||||
if (!this.animationId_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = this.canvas_.width;
|
||||
const height = this.canvas_.height;
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// State machine for watermark phases
|
||||
switch (phase) {
|
||||
case 'fadeIn':
|
||||
currentAlpha = Math.min(config.alpha, currentAlpha + fadeSpeed);
|
||||
if (currentAlpha >= config.alpha) {
|
||||
phase = 'display';
|
||||
}
|
||||
break;
|
||||
case 'display':
|
||||
if (--displayFrames <= 0) {
|
||||
phase = 'fadeOut';
|
||||
}
|
||||
break;
|
||||
case 'fadeOut':
|
||||
currentAlpha = Math.max(0, currentAlpha - fadeSpeed);
|
||||
if (currentAlpha <= 0) {
|
||||
phase = 'transition';
|
||||
currentPosition = getNextPosition();
|
||||
displayFrames = Math.round(config.displayDuration * 60);
|
||||
phase = 'fadeIn';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw watermark if visible
|
||||
if (currentAlpha > 0) {
|
||||
ctx.globalAlpha = currentAlpha;
|
||||
ctx.fillStyle = config.color;
|
||||
ctx.font = `${config.size}px Arial`;
|
||||
ctx.fillText(config.text, currentPosition.left, currentPosition.top);
|
||||
}
|
||||
|
||||
// Request next frame if animation is still active
|
||||
if (this.animationId_) {
|
||||
this.animationId_ = requestAnimationFrame(updateWatermark);
|
||||
}
|
||||
};
|
||||
|
||||
// Start the animation loop
|
||||
this.animationId_ = requestAnimationFrame(updateWatermark);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the current watermark from the video and stops any animations.
|
||||
* @export
|
||||
*/
|
||||
removeWatermark() {
|
||||
if (this.animationId_) {
|
||||
cancelAnimationFrame(this.animationId_);
|
||||
this.animationId_ = null;
|
||||
}
|
||||
const ctx = this.getContext2D_();
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
ctx.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the watermark instance and cleans up the canvas element.
|
||||
* @override
|
||||
*/
|
||||
release() {
|
||||
if (this.canvas_ && this.canvas_.parentNode) {
|
||||
this.canvas_.parentNode.removeChild(this.canvas_);
|
||||
}
|
||||
|
||||
// Clean up resize observer if it exists
|
||||
if (this.resizeObserver_) {
|
||||
this.resizeObserver_.disconnect();
|
||||
this.resizeObserver_ = null;
|
||||
} else {
|
||||
// Remove window resize listener if we were using that
|
||||
window.removeEventListener('resize', () => this.resizeCanvas_());
|
||||
}
|
||||
|
||||
super.release();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user