From 3602b95e8841eca0a68561782611c574522fc552 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:35:07 +0000 Subject: [PATCH 01/77] Bump tar-fs from 3.0.9 to 3.1.1 Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.9 to 3.1.1. - [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.9...v3.1.1) --- updated-dependencies: - dependency-name: tar-fs dependency-version: 3.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90b68bdb..994a686c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8041,9 +8041,9 @@ "dev": true }, "node_modules/tar-fs": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", - "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, "license": "MIT", "dependencies": { From 2cb553f0cbecd0f0f50a802f1fb557e75eebfec5 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Thu, 16 Oct 2025 17:56:26 +0100 Subject: [PATCH 02/77] #692 Parse timestamp tags in WebVTT These get sanitized by DOMPurify, we need to put them back --- scripts/validate.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/validate.js b/scripts/validate.js index 418f749e..54ec1cca 100644 --- a/scripts/validate.js +++ b/scripts/validate.js @@ -185,8 +185,9 @@ var validate = { var processedLangTags = postProcessing.postprocessLangTag(processedVTags); var arrowReplaced = processedLangTags.replace(/-->/g, "-->"); + var timestampTagReplaced = arrowReplaced.replace(/<([\d:.]+)>/g, '<$1>'); - var finalContent = arrowReplaced.replace( + var finalContent = timestampTagReplaced.replace( /<\/v>/g, function (match, offset) { return originalVttContent.indexOf(match, offset) !== -1 ? match : ""; From 48a8655723056d5d226853d39a53ee9d786dc71b Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Thu, 23 Oct 2025 20:50:25 -0500 Subject: [PATCH 03/77] Setting max-width does not set width; need video to default to 100% --- styles/ableplayer.css | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/ableplayer.css b/styles/ableplayer.css index 14b67039..8b96face 100644 --- a/styles/ableplayer.css +++ b/styles/ableplayer.css @@ -248,6 +248,7 @@ .able .able-vidcap-container video { max-width: 100%; + width: 100%; display: block; /* Prevents excess height after element */ } From 1e8cb266ac70c373c0afb830f09bd69a622302b7 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Fri, 19 Dec 2025 19:09:10 -0600 Subject: [PATCH 04/77] Hide transcript & description preferences if player does not have those Fixes #707 and #706 --- scripts/buildplayer.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/buildplayer.js b/scripts/buildplayer.js index d4bdff27..0c4f0599 100644 --- a/scripts/buildplayer.js +++ b/scripts/buildplayer.js @@ -355,7 +355,7 @@ // 'tracks', if provided, is a list of tracks to be used as menu items var thisObj, $menu, includeMenuItem, i, $menuItem, prefCat, whichPref, hasDefault, track, - windowOptions, $thisItem, $prevItem, $nextItem; + windowOptions, $thisItem, $prevItem, $nextItem, hasDescription, hasTranscript; thisObj = this; @@ -373,11 +373,19 @@ if (which === 'prefs') { if (this.prefCats.length > 1) { for (i = 0; i < this.prefCats.length; i++) { + console.log( thisObj ); + prefCat = this.prefCats[i]; + hasDescription = ( thisObj.hasDescTracks || thisObj.hasOpenDesc || thisObj.hasClosedDesc ) ? true : false; + hasTranscript = ( thisObj.transcriptType === null ) ? false : true; + + // If this player does not have descriptions or transcripts, do not output that option preferences. + if ( prefCat === 'descriptions' && ! hasDescription || prefCat === 'transcript' && ! hasTranscript ) { + continue; + } $menuItem = $('
  • ',{ 'role': 'menuitem', 'tabindex': '-1' }); - prefCat = this.prefCats[i]; if (prefCat === 'captions') { $menuItem.text(this.tt.prefMenuCaptions); } else if (prefCat === 'descriptions') { From ca9dec7025aa56d42f0295ef106b95fa9df0435c Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Fri, 19 Dec 2025 19:09:29 -0600 Subject: [PATCH 05/77] Remove console.log --- scripts/buildplayer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/buildplayer.js b/scripts/buildplayer.js index 0c4f0599..a4cb1417 100644 --- a/scripts/buildplayer.js +++ b/scripts/buildplayer.js @@ -373,7 +373,6 @@ if (which === 'prefs') { if (this.prefCats.length > 1) { for (i = 0; i < this.prefCats.length; i++) { - console.log( thisObj ); prefCat = this.prefCats[i]; hasDescription = ( thisObj.hasDescTracks || thisObj.hasOpenDesc || thisObj.hasClosedDesc ) ? true : false; hasTranscript = ( thisObj.transcriptType === null ) ? false : true; From 8db667fef0909c05c85b3938c311d8464b2c58c9 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Fri, 19 Dec 2025 19:21:19 -0600 Subject: [PATCH 06/77] Prevent descriptions from firing when media not playing Fixes #703. Props @redactieuvw --- scripts/description.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/description.js b/scripts/description.js index 4c6931a1..679978d4 100644 --- a/scripts/description.js +++ b/scripts/description.js @@ -449,7 +449,7 @@ }; AblePlayer.prototype.showDescription = function(now) { - if (!this.hasClosedDesc || this.swappingSrc || !this.descOn || ( this.descMethod === 'video' && !this.prefDescVisible ) ) { + if (!this.playing || !this.hasClosedDesc || this.swappingSrc || !this.descOn || ( this.descMethod === 'video' && !this.prefDescVisible ) ) { return; } From 4f1109163b40c7a27dc29ed0e9038af02a161203 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Sat, 27 Dec 2025 13:22:17 -0600 Subject: [PATCH 07/77] Avoid repainting icons when unchanged Fixes #694 --- scripts/control.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/control.js b/scripts/control.js index 78edbece..5dbe5c25 100644 --- a/scripts/control.js +++ b/scripts/control.js @@ -1411,12 +1411,18 @@ AblePlayer.prototype.getIcon = function( $button, id, forceImg = false ) { // Remove existing HTML before generating. - $button.find('svg, img, span:not(.able-clipped)').remove(); // iconData: [0 = svg viewbox, 1 = svg path, 2 = icon font class, 3 = image file] - var iconData = this.getIconData( id ); var iconType = this.iconType; + var iconData = this.getIconData( id ); iconType = ( null === iconData[3] ) ? 'svg' : iconType; - iconType = ( forceImg === true ) ? 'image' : iconType; + iconType = ( forceImg === true ) ? 'img' : iconType; + + var existingIcon = $button.find( iconType + '#ableplayer-' + id ); + // Avoid repainting icon if there's no change. + if ( existingIcon.length > 0 ) { + return; + } + $button.find('svg, img, span:not(.able-clipped)').remove(); if (iconType === 'font') { var $buttonIcon = $('', { @@ -1438,6 +1444,7 @@ icon.setAttribute( 'focusable', 'false' ); icon.setAttribute( 'aria-hidden', 'true'); icon.setAttribute( 'viewBox', iconData[0] ); + icon.setAttribute( 'id', 'ableplayer-' + id ); let path = getNode( 'path', { d: iconData[1] } ); icon.appendChild( path ); From c3cd4b8bd8674307ac3217eb39de4631b307d725 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Sat, 27 Dec 2025 18:02:35 -0600 Subject: [PATCH 08/77] Add a new method to fetch translations, with support for fallback. With this change, translations do not *depend* on translations being present, and while missing translations with log console errors, they will not error out the player. Fixes #682. Also helps with maintenance and translation, as the actual text is available when manipulating code. --- scripts/ableplayer-base.js | 8 +- scripts/buildplayer.js | 76 ++++++++--------- scripts/caption.js | 30 +++---- scripts/chapters.js | 4 +- scripts/control.js | 67 ++++++++------- scripts/description.js | 10 +-- scripts/dragdrop.js | 37 +++++---- scripts/preference.js | 162 ++++++++++++++++++------------------- scripts/search.js | 38 ++++----- scripts/sign.js | 2 +- scripts/transcript.js | 12 +-- scripts/translation.js | 60 +++++++++----- scripts/volume.js | 4 +- translations/ca.json | 1 + translations/cs.json | 1 + translations/da.json | 1 + translations/de.json | 1 + translations/en.json | 1 + translations/es.json | 1 + translations/fr.json | 1 + translations/he.json | 1 + translations/id.json | 1 + translations/it.json | 1 + translations/ja.json | 1 + translations/ms.json | 1 + translations/nb.json | 1 + translations/nl.json | 1 + translations/pl.json | 1 + translations/pt-br.json | 1 + translations/pt.json | 1 + translations/sv.json | 1 + translations/tr.json | 1 + translations/zh-tw.json | 1 + 33 files changed, 289 insertions(+), 241 deletions(-) diff --git a/scripts/ableplayer-base.js b/scripts/ableplayer-base.js index 61f203af..ce516081 100644 --- a/scripts/ableplayer-base.js +++ b/scripts/ableplayer-base.js @@ -471,13 +471,7 @@ var AblePlayerInstances = []; var thisObj = this; $.when(this.getTranslationText()).then( function () { - if (thisObj.countProperties(thisObj.tt) > 50) { - // close enough to ensure that most text variables are populated - thisObj.setup(); - } else { - // can't continue loading player with no text - thisObj.provideFallback(); - } + thisObj.setup(); } ). fail(function() { diff --git a/scripts/buildplayer.js b/scripts/buildplayer.js index a4cb1417..f901d8aa 100644 --- a/scripts/buildplayer.js +++ b/scripts/buildplayer.js @@ -74,7 +74,7 @@ this.$headingDiv = $('<' + headingType + '>'); this.$ableDiv.prepend(this.$headingDiv); this.$headingDiv.addClass('able-offscreen'); - this.$headingDiv.text(this.tt.playerHeading); + this.$headingDiv.text( this.translate( 'playerHeading', 'Media player' ) ); } }; @@ -87,7 +87,7 @@ this.$bigPlayButton = $('' ); - $alertDismiss.attr( 'aria-label', this.tt.dismissButton ); + $alertDismiss.attr( 'aria-label', this.translate( 'dismissButton', 'Dismiss' ) ); $alertDismiss.text( '×' ); $alertDismiss.appendTo(this.$alertBox); @@ -386,13 +386,13 @@ 'tabindex': '-1' }); if (prefCat === 'captions') { - $menuItem.text(this.tt.prefMenuCaptions); + $menuItem.text( this.translate( 'prefMenuCaptions', 'Captions' ) ); } else if (prefCat === 'descriptions') { - $menuItem.text(this.tt.prefMenuDescriptions); + $menuItem.text( this.translate( 'prefMenuDescriptions', 'Descriptions' ) ); } else if (prefCat === 'keyboard') { - $menuItem.text(this.tt.prefMenuKeyboard); + $menuItem.text( this.translate( 'prefMenuKeyboard', 'Keyboard' ) ); } else if (prefCat === 'transcript') { - $menuItem.text(this.tt.prefMenuTranscript); + $menuItem.text( this.translate( 'prefMenuTranscript', 'Transcript' ) ); } $menuItem.on('click',function() { whichPref = $(this).text(); @@ -455,7 +455,7 @@ $menuItem = $('
  • ',{ 'role': 'menuitemradio', 'tabindex': '-1', - }).text(this.tt.captionsOff); + }).text( this.translate( 'captionsOff', 'Captions off' ) ); if (this.prefCaptions === 0) { $menuItem.attr('aria-checked','true'); hasDefault = true; @@ -469,15 +469,15 @@ windowOptions = []; windowOptions.push({ 'name': 'move', - 'label': this.tt.windowMove + 'label': this.translate( 'windowMove', 'Move' ) }); windowOptions.push({ 'name': 'resize', - 'label': this.tt.windowResize + 'label': this.translate( 'windowResize', 'Resize' ) }); windowOptions.push({ 'name': 'close', - 'label': this.tt.windowClose + 'label': this.translate( 'windowClose', 'Close' ) }); for (i = 0; i < windowOptions.length; i++) { $menuItem = $('
  • ',{ @@ -934,7 +934,7 @@ if (this.skin == '2020') { // add a full-width seek bar $sliderDiv = $('
    '); - sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel; + sliderLabel = this.mediaType + ' ' + this.translate( 'seekbarLabel', 'timeline' ); this.$controllerDiv.append($sliderDiv); this.seekBar = new AccessibleSlider($sliderDiv, 'horizontal', baseSliderWidth, 0, this.duration, this.seekInterval, sliderLabel, 'seekbar', true, 'visible'); } @@ -960,7 +960,7 @@ control = controls[j]; if (control === 'seek') { $sliderDiv = $('
    '); - sliderLabel = this.mediaType + ' ' + this.tt.seekbarLabel; + sliderLabel = this.mediaType + ' ' + this.translate( 'seekbarLabel', 'timeline' ); $controllerSpan.append($sliderDiv); if (typeof this.duration === 'undefined' || this.duration === 0) { // set arbitrary starting duration, and change it when duration is known @@ -1137,9 +1137,9 @@ if (!this.prefCaptions || this.prefCaptions !== 1) { // captions are available, but user has them turned off if (this.captions.length > 1) { - captionLabel = this.tt.captions; + captionLabel = this.translate( 'captions', 'Captions' ); } else { - captionLabel = this.tt.showCaptions; + captionLabel = this.translate( 'showCaptions', 'Show captions' ); } $newButton.addClass('buttonOff').attr('title',captionLabel); $newButton.attr('aria-pressed', 'false'); @@ -1149,7 +1149,7 @@ // user prefer non-audio described version // Therefore, load media without description // Description can be toggled on later with this button - $newButton.addClass('buttonOff').attr('title',this.tt.turnOnDescriptions); + $newButton.addClass('buttonOff').attr( 'title', this.translate( 'turnOnDescriptions', 'Turn on descriptions' ) ); } } @@ -1191,7 +1191,7 @@ this.$transcriptButton = $newButton; // gray out transcript button if transcript is not active if (!(this.$transcriptDiv.is(':visible'))) { - this.$transcriptButton.addClass('buttonOff').attr('title',this.tt.showTranscript); + this.$transcriptButton.addClass('buttonOff').attr( 'title', this.translate( 'showTranscript', 'Show transcript' ) ); } } else if (control === 'fullscreen') { this.$fullscreenButton = $newButton; @@ -1508,47 +1508,47 @@ AblePlayer.prototype.getButtonTitle = function(control) { if (control === 'playpause') { - return this.tt.play; + return this.translate( 'play', 'Play' ); } else if (control === 'play') { - return this.tt.play; + return this.translate( 'play', 'Play' ); } else if (control === 'pause') { - return this.tt.pause; + return this.translate( 'pause', 'Pause' ); } else if (control === 'restart') { - return this.tt.restart; + return this.translate( 'restart', 'Restart' ); } else if (control === 'previous') { - return this.tt.prevTrack; + return this.translate( 'prevTrack', 'Previous track' ); } else if (control === 'next') { - return this.tt.nextTrack; + return this.translate( 'nextTrack', 'Next track' ); } else if (control === 'rewind') { - return this.tt.rewind; + return this.translate( 'rewind', 'Rewind' ); } else if (control === 'forward') { - return this.tt.forward; + return this.translate( 'forward', 'Forward' ); } else if (control === 'captions') { if (this.captions.length > 1) { - return this.tt.captions; + return this.translate( 'captions', 'Captions' ); } else { - return (this.captionsOn) ? this.tt.hideCaptions : this.tt.showCaptions; + return (this.captionsOn) ? this.translate( 'hideCaptions', 'Hide captions' ) : this.translate( 'showCaptions', 'Show captions' ); } } else if (control === 'descriptions') { - return (this.descOn) ? this.tt.turnOffDescriptions : this.tt.turnOnDescriptions; + return (this.descOn) ? this.translate( 'turnOffDescriptions', 'Turn off descriptions' ) : this.translate( 'turnOnDescriptions', 'Turn on descriptions' ); } else if (control === 'transcript') { - return (this.$transcriptDiv.is(':visible')) ? this.tt.hideTranscript : this.tt.showTranscript; + return (this.$transcriptDiv.is(':visible')) ? this.translate( 'hideTranscript', 'Hide transcript' ) : this.translate( 'showTranscript', 'Show transcript' ); } else if (control === 'chapters') { - return this.tt.chapters; + return this.translate( 'chapters', 'Chapters' ); } else if (control === 'sign') { - return this.tt.sign; + return this.translate( 'sign', 'Sign language' ); } else if (control === 'volume') { - return this.tt.volume; + return this.translate( 'volume', 'Volume' ); } else if (control === 'faster') { - return this.tt.faster; + return this.translate( 'faster', 'Faster' ); } else if (control === 'slower') { - return this.tt.slower; + return this.translate( 'slower', 'Slower' ); } else if (control === 'preferences') { - return this.tt.preferences; + return this.translate( 'preferences', 'Preferences' ); } else if (control === 'help') { - // return this.tt.help; + // return this.translate( 'help', 'Help' ); } else if (control === 'fullscreen') { - return (!this.fullscreen) ? this.tt.enterFullScreen : this.tt.exitFullScreen; + return ( !this.fullscreen ) ? this.translate( 'enterFullScreen', 'Enter full screen' ) : this.translate( 'exitFullScreen', 'Exit full screen' ); } else { // there should be no other controls, but just in case: // return the name of the control with first letter in upper case diff --git a/scripts/caption.js b/scripts/caption.js index 0e13cf74..ba31eed4 100644 --- a/scripts/caption.js +++ b/scripts/caption.js @@ -278,24 +278,24 @@ switch (pref) { case "prefCaptionsFont": - options[0] = ["serif", this.tt.serif]; - options[1] = ["sans-serif", this.tt.sans]; - options[2] = ["cursive", this.tt.cursive]; - options[3] = ["fantasy", this.tt.fantasy]; - options[4] = ["monospace", this.tt.monospace]; + options[0] = ["serif", this.translate( 'serif', 'serif' )]; + options[1] = ["sans-serif", this.translate( 'sans', 'sans-serif' )]; + options[2] = ["cursive", this.translate( 'cursive', 'cursive' )]; + options[3] = ["fantasy", this.translate( 'fantasy', 'fantasy' )]; + options[4] = ["monospace", this.translate( 'monospace', 'monospace' )]; break; case "prefCaptionsColor": case "prefCaptionsBGColor": // HTML color values must be in English - options[0] = ["white", this.tt.white]; - options[1] = ["yellow", this.tt.yellow]; - options[2] = ["green", this.tt.green]; - options[3] = ["cyan", this.tt.cyan]; - options[4] = ["blue", this.tt.blue]; - options[5] = ["magenta", this.tt.magenta]; - options[6] = ["red", this.tt.red]; - options[7] = ["black", this.tt.black]; + options[0] = ["white", this.translate( 'white', 'white' )]; + options[1] = ["yellow", this.translate( 'yellow', 'yellow' )]; + options[2] = ["green", this.translate( 'green', 'green' )]; + options[3] = ["cyan", this.translate( 'cyan', 'cyan' )]; + options[4] = ["blue", this.translate( 'blue', 'blue' )]; + options[5] = ["magenta", this.translate( 'magenta', 'magenta' )]; + options[6] = ["red", this.translate( 'red', 'red' )]; + options[7] = ["black", this.translate( 'black', 'black' )]; break; case "prefCaptionsSize": @@ -315,8 +315,8 @@ break; case "prefCaptionsStyle": - options[0] = this.tt.captionsStylePopOn; - options[1] = this.tt.captionsStyleRollUp; + options[0] = this.translate( 'captionsStylePopOn', 'Pop-on' ); + options[1] = this.translate( 'captionsStyleRollUp', 'Roll-up' ); break; case "prefCaptionsPosition": diff --git a/scripts/chapters.js b/scripts/chapters.js index 6302d62f..e67e340a 100644 --- a/scripts/chapters.js +++ b/scripts/chapters.js @@ -28,9 +28,9 @@ this.$chaptersNav = $('