vidply 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@ import {HLSRenderer} from '../renderers/HLSRenderer.js';
16
16
  import {createPlayOverlay} from '../icons/Icons.js';
17
17
  import {i18n} from '../i18n/i18n.js';
18
18
  import {StorageManager} from '../utils/StorageManager.js';
19
+ import {DraggableResizable} from '../utils/DraggableResizable.js';
19
20
 
20
21
  export class Player extends EventEmitter {
21
22
  constructor(element, options = {}) {
@@ -214,6 +215,7 @@ export class Player extends EventEmitter {
214
215
  this.originalAudioDescriptionSource = null;
215
216
  // Store caption tracks that should be swapped for audio description
216
217
  this.audioDescriptionCaptionTracks = [];
218
+ this._audioDescriptionDesiredState = false;
217
219
 
218
220
  // DOM query cache (for performance optimization)
219
221
  this._textTracksCache = null;
@@ -396,7 +398,6 @@ export class Player extends EventEmitter {
396
398
  // This allows custom controls to work on iOS devices
397
399
  if (this.element.tagName === 'VIDEO' && this.options.playsInline) {
398
400
  this.element.setAttribute('playsinline', '');
399
- this.element.setAttribute('webkit-playsinline', ''); // For older iOS versions
400
401
  this.element.playsInline = true; // Property version
401
402
  }
402
403
 
@@ -437,6 +438,16 @@ export class Player extends EventEmitter {
437
438
  this.toggle();
438
439
  }
439
440
  });
441
+
442
+ this.on('play', () => {
443
+ this.hidePosterOverlay();
444
+ });
445
+
446
+ this.on('timeupdate', () => {
447
+ if (this.state.currentTime > 0) {
448
+ this.hidePosterOverlay();
449
+ }
450
+ });
440
451
  }
441
452
 
442
453
  createPlayButtonOverlay() {
@@ -653,6 +664,33 @@ export class Player extends EventEmitter {
653
664
  return this.trackElements.find(el => el.track === track);
654
665
  }
655
666
 
667
+ showPosterOverlay() {
668
+ if (!this.videoWrapper || this.element.tagName !== 'VIDEO') {
669
+ return;
670
+ }
671
+
672
+ const poster =
673
+ this.element.getAttribute('poster') ||
674
+ this.element.poster ||
675
+ this.options.poster;
676
+
677
+ if (!poster) {
678
+ return;
679
+ }
680
+
681
+ this.videoWrapper.style.setProperty('--vidply-poster-image', `url("${poster}")`);
682
+ this.videoWrapper.classList.add('vidply-forced-poster');
683
+ }
684
+
685
+ hidePosterOverlay() {
686
+ if (!this.videoWrapper) {
687
+ return;
688
+ }
689
+
690
+ this.videoWrapper.classList.remove('vidply-forced-poster');
691
+ this.videoWrapper.style.removeProperty('--vidply-poster-image');
692
+ }
693
+
656
694
  /**
657
695
  * Set a managed timeout that will be cleaned up on destroy
658
696
  * @param {Function} callback - Callback function
@@ -1048,6 +1086,11 @@ export class Player extends EventEmitter {
1048
1086
  // Store current playback state
1049
1087
  const currentTime = this.state.currentTime;
1050
1088
  const wasPlaying = this.state.playing;
1089
+ const shouldKeepPoster = !wasPlaying && currentTime === 0;
1090
+
1091
+ if (shouldKeepPoster) {
1092
+ this.showPosterOverlay();
1093
+ }
1051
1094
 
1052
1095
  // Store swapped tracks for transcript reload (declare at function scope)
1053
1096
  let swappedTracksForTranscript = [];
@@ -1316,8 +1359,17 @@ export class Player extends EventEmitter {
1316
1359
  if (sourceInfo.descSrc) {
1317
1360
  newSource.setAttribute('data-desc-src', sourceInfo.descSrc);
1318
1361
  }
1319
- this.element.appendChild(newSource);
1362
+ const firstTrack = this.element.querySelector('track');
1363
+ if (firstTrack) {
1364
+ this.element.insertBefore(newSource, firstTrack);
1365
+ } else {
1366
+ this.element.appendChild(newSource);
1367
+ }
1320
1368
  });
1369
+
1370
+ // Ensure cached source references are refreshed after rebuilding the list
1371
+ this._sourceElementsDirty = true;
1372
+ this._sourceElementsCache = null;
1321
1373
 
1322
1374
  // Force reload by calling load() on the element
1323
1375
  // This should pick up the new src attributes from the re-added source elements
@@ -1336,28 +1388,23 @@ export class Player extends EventEmitter {
1336
1388
  // Wait a bit more for tracks to be recognized and loaded after video metadata loads
1337
1389
  await new Promise(resolve => setTimeout(resolve, 300));
1338
1390
 
1339
- // Hide poster if video hasn't started yet (poster should hide when we seek or play)
1340
- if (this.element.tagName === 'VIDEO' && currentTime === 0 && !wasPlaying) {
1341
- // Force poster to hide by doing a minimal seek or loading first frame
1342
- // Setting readyState check or seeking to 0.001 seconds will hide the poster
1343
- if (this.element.readyState >= 1) { // HAVE_METADATA
1344
- // Seek to a tiny fraction to trigger poster hiding without actually moving
1345
- this.element.currentTime = 0.001;
1346
- // Then seek back to 0 after a brief moment to ensure poster stays hidden
1347
- setTimeout(() => {
1348
- this.element.currentTime = 0;
1349
- }, 10);
1350
- }
1391
+ // Restore playback position (avoid forcing first frame if still at start)
1392
+ if (currentTime > 0) {
1393
+ this.seek(currentTime);
1351
1394
  }
1352
1395
 
1353
- // Restore playback position
1354
- this.seek(currentTime);
1355
-
1356
1396
  if (wasPlaying) {
1357
1397
  this.play();
1358
1398
  }
1359
1399
 
1400
+ if (!shouldKeepPoster) {
1401
+ this.hidePosterOverlay();
1402
+ }
1403
+
1360
1404
  // Update state and emit event
1405
+ if (!this._audioDescriptionDesiredState) {
1406
+ return;
1407
+ }
1361
1408
  this.state.audioDescriptionEnabled = true;
1362
1409
  this.emit('audiodescriptionenabled');
1363
1410
  } else {
@@ -1606,8 +1653,10 @@ export class Player extends EventEmitter {
1606
1653
  }
1607
1654
  }
1608
1655
 
1609
- // Restore playback position
1610
- this.seek(currentTime);
1656
+ // Restore playback position (avoid forcing first frame if still at start)
1657
+ if (currentTime > 0) {
1658
+ this.seek(currentTime);
1659
+ }
1611
1660
 
1612
1661
  if (wasPlaying) {
1613
1662
  this.play();
@@ -1854,6 +1903,14 @@ export class Player extends EventEmitter {
1854
1903
  }
1855
1904
  }
1856
1905
 
1906
+ if (!shouldKeepPoster) {
1907
+ this.hidePosterOverlay();
1908
+ }
1909
+
1910
+ if (!this._audioDescriptionDesiredState) {
1911
+ return;
1912
+ }
1913
+
1857
1914
  this.state.audioDescriptionEnabled = true;
1858
1915
  this.emit('audiodescriptionenabled');
1859
1916
  }
@@ -1928,8 +1985,17 @@ export class Player extends EventEmitter {
1928
1985
  if (sourceInfo.descSrc) {
1929
1986
  newSource.setAttribute('data-desc-src', sourceInfo.descSrc);
1930
1987
  }
1931
- this.element.appendChild(newSource);
1988
+ const firstTrack = this.element.querySelector('track');
1989
+ if (firstTrack) {
1990
+ this.element.insertBefore(newSource, firstTrack);
1991
+ } else {
1992
+ this.element.appendChild(newSource);
1993
+ }
1932
1994
  });
1995
+
1996
+ // Ensure cached source references are refreshed after rebuilding the list
1997
+ this._sourceElementsDirty = true;
1998
+ this._sourceElementsCache = null;
1933
1999
 
1934
2000
  // Force reload
1935
2001
  this.element.load();
@@ -1940,20 +2006,29 @@ export class Player extends EventEmitter {
1940
2006
  this.element.load();
1941
2007
  }
1942
2008
 
1943
- // Wait for new source to load
1944
- await new Promise((resolve) => {
1945
- const onLoadedMetadata = () => {
1946
- this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
1947
- resolve();
1948
- };
1949
- this.element.addEventListener('loadedmetadata', onLoadedMetadata);
1950
- });
2009
+ if (currentTime > 0 || wasPlaying) {
2010
+ // Wait for new source to load
2011
+ await new Promise((resolve) => {
2012
+ const onLoadedMetadata = () => {
2013
+ this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
2014
+ resolve();
2015
+ };
2016
+ this.element.addEventListener('loadedmetadata', onLoadedMetadata);
2017
+ });
1951
2018
 
1952
- // Restore playback position
1953
- this.seek(currentTime);
2019
+ if (currentTime > 0) {
2020
+ this.seek(currentTime);
2021
+ }
1954
2022
 
1955
- if (wasPlaying) {
1956
- this.play();
2023
+ if (wasPlaying) {
2024
+ this.play();
2025
+ }
2026
+ }
2027
+
2028
+ if (!wasPlaying && currentTime === 0) {
2029
+ this.showPosterOverlay();
2030
+ } else {
2031
+ this.hidePosterOverlay();
1957
2032
  }
1958
2033
 
1959
2034
  // Reload transcript if visible (after video metadata loaded, tracks should be available)
@@ -1967,6 +2042,10 @@ export class Player extends EventEmitter {
1967
2042
  }, 500);
1968
2043
  }
1969
2044
 
2045
+ if (this._audioDescriptionDesiredState) {
2046
+ return;
2047
+ }
2048
+
1970
2049
  this.state.audioDescriptionEnabled = false;
1971
2050
  this.emit('audiodescriptiondisabled');
1972
2051
  }
@@ -1982,10 +2061,12 @@ export class Player extends EventEmitter {
1982
2061
  if (descriptionTrack && hasAudioDescriptionSrc) {
1983
2062
  // We have both: toggle description track AND swap caption tracks/sources
1984
2063
  if (this.state.audioDescriptionEnabled) {
2064
+ this._audioDescriptionDesiredState = false;
1985
2065
  // Disable: toggle description track off and swap captions/sources back
1986
2066
  descriptionTrack.mode = 'hidden';
1987
2067
  await this.disableAudioDescription();
1988
2068
  } else {
2069
+ this._audioDescriptionDesiredState = true;
1989
2070
  // Enable: swap caption tracks/sources and toggle description track on
1990
2071
  await this.enableAudioDescription();
1991
2072
  // Wait for tracks to be ready after source swap, then enable description track
@@ -2022,10 +2103,12 @@ export class Player extends EventEmitter {
2022
2103
  // Only description track, no audio-described video source to swap
2023
2104
  // Toggle description track
2024
2105
  if (descriptionTrack.mode === 'showing') {
2106
+ this._audioDescriptionDesiredState = false;
2025
2107
  descriptionTrack.mode = 'hidden';
2026
2108
  this.state.audioDescriptionEnabled = false;
2027
2109
  this.emit('audiodescriptiondisabled');
2028
2110
  } else {
2111
+ this._audioDescriptionDesiredState = true;
2029
2112
  descriptionTrack.mode = 'showing';
2030
2113
  this.state.audioDescriptionEnabled = true;
2031
2114
  this.emit('audiodescriptionenabled');
@@ -2033,8 +2116,10 @@ export class Player extends EventEmitter {
2033
2116
  } else if (hasAudioDescriptionSrc) {
2034
2117
  // Use audio-described video source (no description track)
2035
2118
  if (this.state.audioDescriptionEnabled) {
2119
+ this._audioDescriptionDesiredState = false;
2036
2120
  await this.disableAudioDescription();
2037
2121
  } else {
2122
+ this._audioDescriptionDesiredState = true;
2038
2123
  await this.enableAudioDescription();
2039
2124
  }
2040
2125
  }
@@ -2164,235 +2249,27 @@ export class Player extends EventEmitter {
2164
2249
  setupSignLanguageInteraction() {
2165
2250
  if (!this.signLanguageWrapper) return;
2166
2251
 
2167
- let isDragging = false;
2168
- let isResizing = false;
2169
- let resizeDirection = null;
2170
- let startX = 0;
2171
- let startY = 0;
2172
- let startLeft = 0;
2173
- let startTop = 0;
2174
- let startWidth = 0;
2175
- let startHeight = 0;
2176
- let dragMode = false;
2177
- let resizeMode = false;
2178
-
2179
- // Mouse drag on video element
2180
- const onMouseDownVideo = (e) => {
2181
- if (e.target !== this.signLanguageVideo) return;
2182
- e.preventDefault();
2183
- isDragging = true;
2184
- startX = e.clientX;
2185
- startY = e.clientY;
2186
- const rect = this.signLanguageWrapper.getBoundingClientRect();
2187
- startLeft = rect.left;
2188
- startTop = rect.top;
2189
- this.signLanguageWrapper.classList.add('vidply-sign-dragging');
2190
- };
2191
-
2192
- // Mouse resize on handles
2193
- const onMouseDownHandle = (e) => {
2194
- if (!e.target.classList.contains('vidply-sign-resize-handle')) return;
2195
- e.preventDefault();
2196
- e.stopPropagation();
2197
- isResizing = true;
2198
- resizeDirection = e.target.getAttribute('data-direction');
2199
- startX = e.clientX;
2200
- startY = e.clientY;
2201
- const rect = this.signLanguageWrapper.getBoundingClientRect();
2202
- startLeft = rect.left;
2203
- startTop = rect.top;
2204
- startWidth = rect.width;
2205
- startHeight = rect.height;
2206
- this.signLanguageWrapper.classList.add('vidply-sign-resizing');
2207
- };
2208
-
2209
- const onMouseMove = (e) => {
2210
- if (isDragging) {
2211
- const deltaX = e.clientX - startX;
2212
- const deltaY = e.clientY - startY;
2213
-
2214
- // Get videoWrapper and container dimensions
2215
- const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
2216
- const containerRect = this.container.getBoundingClientRect();
2217
- const wrapperRect = this.signLanguageWrapper.getBoundingClientRect();
2218
-
2219
- // Calculate videoWrapper position relative to container
2220
- const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
2221
- const videoWrapperTop = videoWrapperRect.top - containerRect.top;
2222
-
2223
- // Calculate new position (in client coordinates)
2224
- let newLeft = startLeft + deltaX - containerRect.left;
2225
- let newTop = startTop + deltaY - containerRect.top;
2226
-
2227
- const controlsHeight = 95; // Height of controls when visible
2228
-
2229
- // Constrain to videoWrapper bounds (ensuring it stays above controls)
2230
- newLeft = Math.max(videoWrapperLeft, Math.min(newLeft, videoWrapperLeft + videoWrapperRect.width - wrapperRect.width));
2231
- newTop = Math.max(videoWrapperTop, Math.min(newTop, videoWrapperTop + videoWrapperRect.height - wrapperRect.height - controlsHeight));
2232
-
2233
- this.signLanguageWrapper.style.left = `${newLeft}px`;
2234
- this.signLanguageWrapper.style.top = `${newTop}px`;
2235
- this.signLanguageWrapper.style.right = 'auto';
2236
- this.signLanguageWrapper.style.bottom = 'auto';
2237
- // Remove position classes
2238
- this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
2239
- } else if (isResizing) {
2240
- const deltaX = e.clientX - startX;
2241
-
2242
- // Get videoWrapper and container dimensions
2243
- const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
2244
- const containerRect = this.container.getBoundingClientRect();
2245
-
2246
- let newWidth = startWidth;
2247
- let newLeft = startLeft - containerRect.left;
2248
-
2249
- // Only resize width, let height auto-adjust to maintain aspect ratio
2250
- if (resizeDirection.includes('e')) {
2251
- newWidth = Math.max(150, startWidth + deltaX);
2252
- // Constrain width to not exceed videoWrapper right edge
2253
- const maxWidth = (videoWrapperRect.right - startLeft);
2254
- newWidth = Math.min(newWidth, maxWidth);
2255
- }
2256
- if (resizeDirection.includes('w')) {
2257
- const proposedWidth = Math.max(150, startWidth - deltaX);
2258
- const proposedLeft = startLeft + (startWidth - proposedWidth) - containerRect.left;
2259
- // Constrain to not go beyond videoWrapper left edge
2260
- const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
2261
- if (proposedLeft >= videoWrapperLeft) {
2262
- newWidth = proposedWidth;
2263
- newLeft = proposedLeft;
2264
- }
2265
- }
2266
-
2267
- this.signLanguageWrapper.style.width = `${newWidth}px`;
2268
- this.signLanguageWrapper.style.height = 'auto'; // Let video maintain aspect ratio
2269
- if (resizeDirection.includes('w')) {
2270
- this.signLanguageWrapper.style.left = `${newLeft}px`;
2271
- }
2272
- this.signLanguageWrapper.style.right = 'auto';
2273
- this.signLanguageWrapper.style.bottom = 'auto';
2274
- // Remove position classes
2275
- this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
2276
- }
2277
- };
2278
-
2279
- const onMouseUp = () => {
2280
- if (isDragging || isResizing) {
2281
- this.saveSignLanguagePreferences();
2282
- }
2283
- isDragging = false;
2284
- isResizing = false;
2285
- resizeDirection = null;
2286
- this.signLanguageWrapper.classList.remove('vidply-sign-dragging', 'vidply-sign-resizing');
2287
- };
2288
-
2289
- // Keyboard controls
2290
- const onKeyDown = (e) => {
2291
- // Toggle drag mode with D key
2292
- if (e.key === 'd' || e.key === 'D') {
2293
- dragMode = !dragMode;
2294
- resizeMode = false;
2295
- this.signLanguageWrapper.classList.toggle('vidply-sign-keyboard-drag', dragMode);
2296
- this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-resize');
2297
- e.preventDefault();
2298
- return;
2299
- }
2300
-
2301
- // Toggle resize mode with R key
2302
- if (e.key === 'r' || e.key === 'R') {
2303
- resizeMode = !resizeMode;
2304
- dragMode = false;
2305
- this.signLanguageWrapper.classList.toggle('vidply-sign-keyboard-resize', resizeMode);
2306
- this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-drag');
2307
- e.preventDefault();
2308
- return;
2309
- }
2310
-
2311
- // Escape to exit modes
2312
- if (e.key === 'Escape') {
2313
- dragMode = false;
2314
- resizeMode = false;
2315
- this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-drag', 'vidply-sign-keyboard-resize');
2316
- e.preventDefault();
2317
- return;
2318
- }
2319
-
2320
- // Arrow keys for drag/resize
2321
- if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
2322
- const step = e.shiftKey ? 10 : 5;
2323
- const rect = this.signLanguageWrapper.getBoundingClientRect();
2324
-
2325
- // Get videoWrapper and container bounds
2326
- const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
2327
- const containerRect = this.container.getBoundingClientRect();
2328
-
2329
- // Calculate videoWrapper position relative to container
2330
- const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
2331
- const videoWrapperTop = videoWrapperRect.top - containerRect.top;
2332
-
2333
- if (dragMode) {
2334
- // Get current position relative to container
2335
- let left = rect.left - containerRect.left;
2336
- let top = rect.top - containerRect.top;
2337
-
2338
- if (e.key === 'ArrowLeft') left -= step;
2339
- if (e.key === 'ArrowRight') left += step;
2340
- if (e.key === 'ArrowUp') top -= step;
2341
- if (e.key === 'ArrowDown') top += step;
2342
-
2343
- const controlsHeight = 95; // Height of controls when visible
2344
-
2345
- // Constrain to videoWrapper bounds (ensuring it stays above controls)
2346
- left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperRect.width - rect.width));
2347
- top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperRect.height - rect.height - controlsHeight));
2348
-
2349
- this.signLanguageWrapper.style.left = `${left}px`;
2350
- this.signLanguageWrapper.style.top = `${top}px`;
2351
- this.signLanguageWrapper.style.right = 'auto';
2352
- this.signLanguageWrapper.style.bottom = 'auto';
2353
- // Remove position classes
2354
- this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
2355
- this.saveSignLanguagePreferences();
2356
- e.preventDefault();
2357
- } else if (resizeMode) {
2358
- let width = rect.width;
2359
-
2360
- // Only adjust width, height will auto-adjust to maintain aspect ratio
2361
- if (e.key === 'ArrowLeft') width -= step;
2362
- if (e.key === 'ArrowRight') width += step;
2363
- // Up/Down also adjusts width for simplicity
2364
- if (e.key === 'ArrowUp') width += step;
2365
- if (e.key === 'ArrowDown') width -= step;
2366
-
2367
- // Constrain width
2368
- width = Math.max(150, width);
2369
- // Don't let it exceed videoWrapper width
2370
- width = Math.min(width, videoWrapperRect.width);
2371
-
2372
- this.signLanguageWrapper.style.width = `${width}px`;
2373
- this.signLanguageWrapper.style.height = 'auto';
2374
- this.saveSignLanguagePreferences();
2375
- e.preventDefault();
2376
- }
2377
- }
2378
- };
2379
-
2380
- // Attach event listeners
2381
- this.signLanguageVideo.addEventListener('mousedown', onMouseDownVideo);
2382
- const handles = this.signLanguageWrapper.querySelectorAll('.vidply-sign-resize-handle');
2383
- handles.forEach(handle => handle.addEventListener('mousedown', onMouseDownHandle));
2384
- document.addEventListener('mousemove', onMouseMove);
2385
- document.addEventListener('mouseup', onMouseUp);
2386
- this.signLanguageWrapper.addEventListener('keydown', onKeyDown);
2252
+ // Get resize handles
2253
+ const resizeHandles = Array.from(this.signLanguageWrapper.querySelectorAll('.vidply-sign-resize-handle'));
2254
+
2255
+ // Create DraggableResizable utility
2256
+ this.signLanguageDraggable = new DraggableResizable(this.signLanguageWrapper, {
2257
+ dragHandle: this.signLanguageVideo,
2258
+ resizeHandles: resizeHandles,
2259
+ constrainToViewport: true,
2260
+ maintainAspectRatio: true,
2261
+ minWidth: 150,
2262
+ minHeight: 100,
2263
+ classPrefix: 'vidply-sign',
2264
+ keyboardDragKey: 'd',
2265
+ keyboardResizeKey: 'r',
2266
+ keyboardStep: 5,
2267
+ keyboardStepLarge: 10
2268
+ });
2387
2269
 
2388
2270
  // Store for cleanup
2389
2271
  this.signLanguageInteractionHandlers = {
2390
- mouseDownVideo: onMouseDownVideo,
2391
- mouseDownHandle: onMouseDownHandle,
2392
- mouseMove: onMouseMove,
2393
- mouseUp: onMouseUp,
2394
- keyDown: onKeyDown,
2395
- handles
2272
+ draggable: this.signLanguageDraggable
2396
2273
  };
2397
2274
  }
2398
2275
 
@@ -2483,23 +2360,14 @@ export class Player extends EventEmitter {
2483
2360
  this.signLanguageHandlers = null;
2484
2361
  }
2485
2362
 
2486
- // Remove interaction handlers
2487
- if (this.signLanguageInteractionHandlers) {
2488
- if (this.signLanguageVideo) {
2489
- this.signLanguageVideo.removeEventListener('mousedown', this.signLanguageInteractionHandlers.mouseDownVideo);
2490
- }
2491
- if (this.signLanguageInteractionHandlers.handles) {
2492
- this.signLanguageInteractionHandlers.handles.forEach(handle => {
2493
- handle.removeEventListener('mousedown', this.signLanguageInteractionHandlers.mouseDownHandle);
2494
- });
2495
- }
2496
- document.removeEventListener('mousemove', this.signLanguageInteractionHandlers.mouseMove);
2497
- document.removeEventListener('mouseup', this.signLanguageInteractionHandlers.mouseUp);
2498
- if (this.signLanguageWrapper) {
2499
- this.signLanguageWrapper.removeEventListener('keydown', this.signLanguageInteractionHandlers.keyDown);
2500
- }
2501
- this.signLanguageInteractionHandlers = null;
2363
+ // Destroy draggable utility
2364
+ if (this.signLanguageDraggable) {
2365
+ this.signLanguageDraggable.destroy();
2366
+ this.signLanguageDraggable = null;
2502
2367
  }
2368
+
2369
+ // Clear interaction handlers reference
2370
+ this.signLanguageInteractionHandlers = null;
2503
2371
 
2504
2372
  // Remove video and wrapper elements
2505
2373
  if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
@@ -2619,7 +2487,10 @@ export class Player extends EventEmitter {
2619
2487
  }
2620
2488
 
2621
2489
  if (this.transcriptManager && this.transcriptManager.isVisible) {
2622
- this.transcriptManager.positionTranscript();
2490
+ // Only auto-position if user hasn't manually moved it
2491
+ if (!this.transcriptManager.draggableResizable || !this.transcriptManager.draggableResizable.manuallyPositioned) {
2492
+ this.transcriptManager.positionTranscript();
2493
+ }
2623
2494
  }
2624
2495
  };
2625
2496
 
@@ -2632,7 +2503,10 @@ export class Player extends EventEmitter {
2632
2503
  // Wait for layout to settle
2633
2504
  setTimeout(() => {
2634
2505
  if (this.transcriptManager && this.transcriptManager.isVisible) {
2635
- this.transcriptManager.positionTranscript();
2506
+ // Only auto-position if user hasn't manually moved it
2507
+ if (!this.transcriptManager.draggableResizable || !this.transcriptManager.draggableResizable.manuallyPositioned) {
2508
+ this.transcriptManager.positionTranscript();
2509
+ }
2636
2510
  }
2637
2511
  }, 100);
2638
2512
  };