Skip to content

Commit

Permalink
feat(UI): Add save video frame button (#6926)
Browse files Browse the repository at this point in the history
Thank you @tykus160 for your Polish translation!
  • Loading branch information
avelad authored Jun 27, 2024
1 parent 767cbed commit 19cfbf9
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 0 deletions.
1 change: 1 addition & 0 deletions build/types/ui
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
+../../ui/remote_button.js
+../../ui/resolution_selection.js
+../../ui/rewind_button.js
+../../ui/save_video_frame_button.js
+../../ui/seek_bar.js
+../../ui/settings_menu.js
+../../ui/small_play_button.js
Expand Down
2 changes: 2 additions & 0 deletions docs/tutorials/ui-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ The following buttons can be added to the overflow menu:
* toggle_stereoscopic: adds a button that toggle between monoscopic and stereoscopic. The button
is visible only if playing a VR content.
* ad_statistics: adds a button that displays ad statistics of the video.
* save_video_frame: adds a button to save the current video frame.
<!-- TODO: If we add more buttons that can be put in the order this way, list them here. -->

Example:
Expand Down Expand Up @@ -141,6 +142,7 @@ The following buttons can be added to the context menu:
that support it. Button is invisible on other browsers. Note that it will use the
[Document Picture-in-Picture API]() if supported.
* ad_statistics: adds a button that displays ad statistics of the video.
* save_video_frame: adds a button to save the current video frame.

Example:
```js
Expand Down
1 change: 1 addition & 0 deletions shaka-player.uncompiled.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ goog.require('shaka.ui.RecenterVRButton');
goog.require('shaka.ui.RemoteButton');
goog.require('shaka.ui.ResolutionSelection');
goog.require('shaka.ui.RewindButton');
goog.require('shaka.ui.SaveVideoFrameButton');
goog.require('shaka.ui.SkipAdButton');
goog.require('shaka.ui.SmallPlayButton');
goog.require('shaka.ui.Spacer');
Expand Down
1 change: 1 addition & 0 deletions ui/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ shaka.ui.Enums.MaterialDesignIcons = {
'STATISTICS_OFF': 'insert_chart',
'RECENTER_VR': 'control_camera',
'TOGGLE_STEREOSCOPIC': '3d_rotation',
'DOWNLOAD': 'download',
};
1 change: 1 addition & 0 deletions ui/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"BACK": "Back",
"CAPTIONS": "Captions",
"CAST": "Cast...",
"DOWNLOAD_VIDEO_FRAME": "Save video frame",
"ENTER_LOOP_MODE": "Loop the current video",
"ENTER_PICTURE_IN_PICTURE": "Enter Picture-in-Picture",
"EXIT_FULL_SCREEN": "Exit full screen",
Expand Down
1 change: 1 addition & 0 deletions ui/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"BACK": "Atrás",
"CAPTIONS": "Subtítulos",
"CAST": "Enviar...",
"DOWNLOAD_VIDEO_FRAME": "Guardar fotograma",
"ENTER_LOOP_MODE": "Reproducir en bucle el vídeo actual",
"ENTER_PICTURE_IN_PICTURE": "Activar el modo imagen en imagen",
"EXIT_FULL_SCREEN": "Salir del modo de pantalla completa",
Expand Down
1 change: 1 addition & 0 deletions ui/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"BACK": "Retour",
"CAPTIONS": "Sous-titres",
"CAST": "Caster…",
"DOWNLOAD_VIDEO_FRAME": "Enregistrer l'image vidéo",
"ENTER_LOOP_MODE": "Lire en boucle la vidéo en cours",
"ENTER_PICTURE_IN_PICTURE": "Utiliser le mode Picture-in-Picture",
"EXIT_FULL_SCREEN": "Quitter le mode plein écran",
Expand Down
1 change: 1 addition & 0 deletions ui/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"BACK": "Wstecz",
"CAPTIONS": "Napisy",
"CAST": "Przesyłaj...",
"DOWNLOAD_VIDEO_FRAME": "Zapisz klatkę wideo",
"ENTER_LOOP_MODE": "Odtwarzaj bieżący film w pętli",
"ENTER_PICTURE_IN_PICTURE": "Włącz tryb obrazu w obrazie",
"EXIT_FULL_SCREEN": "Zamknij pełny ekran",
Expand Down
1 change: 1 addition & 0 deletions ui/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"BACK": "Voltar",
"CAPTIONS": "Legendas",
"CAST": "Transmitir...",
"DOWNLOAD_VIDEO_FRAME": "Salvar quadro de vídeo",
"ENTER_LOOP_MODE": "Repetir o vídeo atual",
"ENTER_PICTURE_IN_PICTURE": "Entrar no modo picture-in-picture",
"EXIT_FULL_SCREEN": "Sair do modo tela cheia",
Expand Down
1 change: 1 addition & 0 deletions ui/locales/pt-PT.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"BACK": "Anterior",
"CAPTIONS": "Legendas",
"CAST": "Transmitir...",
"DOWNLOAD_VIDEO_FRAME": "Salvar quadro de vídeo",
"ENTER_LOOP_MODE": "Repetir o vídeo atual",
"ENTER_PICTURE_IN_PICTURE": "Entrar no modo ecrã no ecrã",
"EXIT_FULL_SCREEN": "Sair do modo de ecrã inteiro",
Expand Down
4 changes: 4 additions & 0 deletions ui/locales/source.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"description": "Label for a button used to open the native Cast dialog in the browser and select a destination to Cast to.",
"message": "Cast..."
},
"DOWNLOAD_VIDEO_FRAME": {
"description": "Label for a button used to download the current video frame.",
"message": "Save video frame"
},
"ENTER_LOOP_MODE": {
"description": "Label for a button that controls the loop mode of a video player. This mode will loop (play over and over) currently selected video.",
"message": "Loop the current video"
Expand Down
196 changes: 196 additions & 0 deletions ui/save_video_frame_button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/


goog.provide('shaka.ui.SaveVideoFrameButton');

goog.require('shaka.ads.Utils');
goog.require('shaka.ui.ContextMenu');
goog.require('shaka.ui.Controls');
goog.require('shaka.ui.Element');
goog.require('shaka.ui.Enums');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.Localization');
goog.require('shaka.ui.OverflowMenu');
goog.require('shaka.ui.Utils');
goog.require('shaka.util.Dom');


/**
* @extends {shaka.ui.Element}
* @final
* @export
*/
shaka.ui.SaveVideoFrameButton = class extends shaka.ui.Element {
/**
* @param {!HTMLElement} parent
* @param {!shaka.ui.Controls} controls
*/
constructor(parent, controls) {
super(parent, controls);

const LocIds = shaka.ui.Locales.Ids;
/** @private {!HTMLButtonElement} */
this.button_ = shaka.util.Dom.createButton();
this.button_.classList.add('shaka-save.video-frame-button');
this.button_.classList.add('shaka-tooltip');

/** @private {!HTMLElement} */
this.icon_ = shaka.util.Dom.createHTMLElement('i');
this.icon_.classList.add('material-icons-round');
this.icon_.textContent = shaka.ui.Enums.MaterialDesignIcons.DOWNLOAD;
this.button_.appendChild(this.icon_);

const label = shaka.util.Dom.createHTMLElement('label');
label.classList.add('shaka-overflow-button-label');
label.classList.add('shaka-overflow-menu-only');
this.nameSpan_ = shaka.util.Dom.createHTMLElement('span');
this.nameSpan_.textContent =
this.localization.resolve(LocIds.DOWNLOAD_VIDEO_FRAME);
label.appendChild(this.nameSpan_);

/** @private {!HTMLElement} */
this.currentState_ = shaka.util.Dom.createHTMLElement('span');
this.currentState_.classList.add('shaka-current-selection-span');
label.appendChild(this.currentState_);

this.button_.appendChild(label);

this.updateLocalizedStrings_();

this.parent.appendChild(this.button_);

this.eventManager.listen(
this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
this.updateLocalizedStrings_();
});

this.eventManager.listen(
this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => {
this.updateLocalizedStrings_();
});

this.eventManager.listen(this.button_, 'click', () => {
this.onClick_();
});

const vr = this.controls.getVR();
this.eventManager.listen(vr, 'vrstatuschanged', () => {
this.checkAvailability_();
});

this.eventManager.listen(
this.adManager, shaka.ads.Utils.AD_STARTED, () => {
this.checkAvailability_();
});

this.eventManager.listen(
this.adManager, shaka.ads.Utils.AD_STOPPED, () => {
this.checkAvailability_();
});

this.eventManager.listen(this.player, 'unloading', () => {
this.checkAvailability_();
});

this.eventManager.listen(this.player, 'loaded', () => {
this.checkAvailability_();
});

this.eventManager.listen(this.player, 'loaded', () => {
this.checkAvailability_();
});

this.eventManager.listen(this.video, 'play', () => {
this.checkAvailability_();
});

this.eventManager.listen(this.video, 'pause', () => {
this.checkAvailability_();
});

this.eventManager.listen(this.video, 'seeking', () => {
this.checkAvailability_();
});

this.checkAvailability_();
}


/** @private */
onClick_() {
const canvas = /** @type {!HTMLCanvasElement}*/ (
document.createElement('canvas'));
const context = /** @type {CanvasRenderingContext2D} */ (
canvas.getContext('2d'));

const video = /** @type {!HTMLVideoElement} */ (
this.controls.getLocalVideo());

canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
context.drawImage(video, 0, 0, canvas.width, canvas.height);

const dataURL = canvas.toDataURL('image/png');

const downloadLink = /** @type {!HTMLAnchorElement}*/ (
document.createElement('a'));
downloadLink.href = dataURL;
downloadLink.download =
'videoframe_' + video.currentTime.toFixed(3) + '.png';
downloadLink.click();
}


/**
* @private
*/
checkAvailability_() {
let available = true;
if (this.controls.isPlayingVR()) {
available = false;
}
if (this.player.drmInfo() || this.player.isAudioOnly()) {
available = false;
}
if (this.ad) {
available = false;
}
shaka.ui.Utils.setDisplay(this.button_, available);
}


/**
* @private
*/
updateLocalizedStrings_() {
const LocIds = shaka.ui.Locales.Ids;

this.button_.ariaLabel =
this.localization.resolve(LocIds.DOWNLOAD_VIDEO_FRAME);
this.nameSpan_.textContent =
this.localization.resolve(LocIds.DOWNLOAD_VIDEO_FRAME);
}
};


/**
* @implements {shaka.extern.IUIElement.Factory}
* @final
*/
shaka.ui.SaveVideoFrameButton.Factory = class {
/** @override */
create(rootElement, controls) {
return new shaka.ui.SaveVideoFrameButton(rootElement, controls);
}
};


shaka.ui.OverflowMenu.registerElement(
'save_video_frame', new shaka.ui.SaveVideoFrameButton.Factory());

shaka.ui.ContextMenu.registerElement(
'save_video_frame', new shaka.ui.SaveVideoFrameButton.Factory());
2 changes: 2 additions & 0 deletions ui/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ shaka.ui.Overlay = class {
'playback_rate',
'recenter_vr',
'toggle_stereoscopic',
'save_video_frame',
],
statisticsList: [
'width',
Expand Down Expand Up @@ -228,6 +229,7 @@ shaka.ui.Overlay = class {
contextMenuElements: [
'loop',
'picture_in_picture',
'save_video_frame',
'statistics',
'ad_statistics',
],
Expand Down

0 comments on commit 19cfbf9

Please sign in to comment.