Home Reference Source

src/controller/id3-track-controller.ts

  1. import { Events } from '../events';
  2. import {
  3. sendAddTrackEvent,
  4. clearCurrentCues,
  5. removeCuesInRange,
  6. } from '../utils/texttrack-utils';
  7. import * as ID3 from '../demux/id3';
  8. import type {
  9. BufferFlushingData,
  10. FragParsingMetadataData,
  11. MediaAttachedData,
  12. } from '../types/events';
  13. import type { ComponentAPI } from '../types/component-api';
  14. import type Hls from '../hls';
  15.  
  16. declare global {
  17. interface Window {
  18. WebKitDataCue: VTTCue | void;
  19. }
  20. }
  21.  
  22. const MIN_CUE_DURATION = 0.25;
  23.  
  24. class ID3TrackController implements ComponentAPI {
  25. private hls: Hls;
  26. private id3Track: TextTrack | null = null;
  27. private media: HTMLMediaElement | null = null;
  28.  
  29. constructor(hls) {
  30. this.hls = hls;
  31. this._registerListeners();
  32. }
  33.  
  34. destroy() {
  35. this._unregisterListeners();
  36. }
  37.  
  38. private _registerListeners() {
  39. const { hls } = this;
  40. hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
  41. hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
  42. hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
  43. hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
  44. }
  45.  
  46. private _unregisterListeners() {
  47. const { hls } = this;
  48. hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
  49. hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
  50. hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this);
  51. hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
  52. }
  53.  
  54. // Add ID3 metatadata text track.
  55. protected onMediaAttached(
  56. event: Events.MEDIA_ATTACHED,
  57. data: MediaAttachedData
  58. ): void {
  59. this.media = data.media;
  60. }
  61.  
  62. protected onMediaDetaching(): void {
  63. if (!this.id3Track) {
  64. return;
  65. }
  66. clearCurrentCues(this.id3Track);
  67. this.id3Track = null;
  68. this.media = null;
  69. }
  70.  
  71. getID3Track(textTracks: TextTrackList): TextTrack | void {
  72. if (!this.media) {
  73. return;
  74. }
  75. for (let i = 0; i < textTracks.length; i++) {
  76. const textTrack: TextTrack = textTracks[i];
  77. if (textTrack.kind === 'metadata' && textTrack.label === 'id3') {
  78. // send 'addtrack' when reusing the textTrack for metadata,
  79. // same as what we do for captions
  80. sendAddTrackEvent(textTrack, this.media);
  81.  
  82. return textTrack;
  83. }
  84. }
  85. return this.media.addTextTrack('metadata', 'id3');
  86. }
  87.  
  88. onFragParsingMetadata(
  89. event: Events.FRAG_PARSING_METADATA,
  90. data: FragParsingMetadataData
  91. ) {
  92. if (!this.media) {
  93. return;
  94. }
  95. const fragment = data.frag;
  96. const samples = data.samples;
  97.  
  98. // create track dynamically
  99. if (!this.id3Track) {
  100. this.id3Track = this.getID3Track(this.media.textTracks) as TextTrack;
  101. this.id3Track.mode = 'hidden';
  102. }
  103.  
  104. // Attempt to recreate Safari functionality by creating
  105. // WebKitDataCue objects when available and store the decoded
  106. // ID3 data in the value property of the cue
  107. const Cue = (self.WebKitDataCue || self.VTTCue || self.TextTrackCue) as any;
  108.  
  109. for (let i = 0; i < samples.length; i++) {
  110. const frames = ID3.getID3Frames(samples[i].data);
  111. if (frames) {
  112. const startTime = samples[i].pts;
  113. let endTime: number =
  114. i < samples.length - 1 ? samples[i + 1].pts : fragment.end;
  115.  
  116. const timeDiff = endTime - startTime;
  117. if (timeDiff <= 0) {
  118. endTime = startTime + MIN_CUE_DURATION;
  119. }
  120.  
  121. for (let j = 0; j < frames.length; j++) {
  122. const frame = frames[j];
  123. // Safari doesn't put the timestamp frame in the TextTrack
  124. if (!ID3.isTimeStampFrame(frame)) {
  125. const cue = new Cue(startTime, endTime, '');
  126. cue.value = frame;
  127. this.id3Track.addCue(cue);
  128. }
  129. }
  130. }
  131. }
  132. }
  133.  
  134. onBufferFlushing(
  135. event: Events.BUFFER_FLUSHING,
  136. { startOffset, endOffset, type }: BufferFlushingData
  137. ) {
  138. if (!type || type === 'audio') {
  139. // id3 cues come from parsed audio only remove cues when audio buffer is cleared
  140. const { id3Track } = this;
  141. if (id3Track) {
  142. removeCuesInRange(id3Track, startOffset, endOffset);
  143. }
  144. }
  145. }
  146. }
  147.  
  148. export default ID3TrackController;