Skip to content

⬅️ Back to Table of Contents

📄 useMediaControls

📊 Analysis Summary

Metric Count
🔧 Functions 7
📦 Imports 17
📊 Variables & Constants 1
⚡ Async/Await Patterns 2
🟢 Vue Composition API 4
📐 Interfaces 5

📚 Table of Contents

🛠️ File Location:

📂 packages/core/useMediaControls/index.ts

📦 Imports

Name Source
EventHookOn @vueuse/shared
Fn @vueuse/shared
MaybeRef vue
MaybeRefOrGetter vue
ShallowRef vue
ConfigurableDocument ../_configurable
createEventHook @vueuse/shared
isObject @vueuse/shared
toRef @vueuse/shared
tryOnScopeDispose @vueuse/shared
watchIgnorable @vueuse/shared
shallowRef vue
toValue vue
watch vue
watchEffect vue
defaultDocument ../_configurable
useEventListener ../useEventListener

Variables & Constants

Name Type Kind Value Exported
defaultOptions UseMediaControlsOptions const { src: '', tracks: [], }

Async/Await Patterns

Type Function Await Expressions Promise Chains
promise-chain useMediaControls none new Promise(...), el.requestPictureInPicture().then(resolve).catch, el.reques...
promise-chain togglePictureInPicture none new Promise(...), el.requestPictureInPicture().then(resolve).catch, el.reques...

Vue Composition API

Name Type Reactive Variables Composables
watch watch none none
watch watch none none
watch watch none none
watch watch none none

Functions

useMediaControls(target: MaybeRef<HTMLMediaElement | null | unde…, options: UseMediaControlsOptions): UseMediaControlsReturn

Parameters:

  • target MaybeRef<HTMLMediaElement | null | undefined>
  • options UseMediaControlsOptions

Returns: UseMediaControlsReturn

Calls:

  • toRef (from @vueuse/shared)
  • shallowRef (from vue)
  • Boolean
  • createEventHook (from @vueuse/shared)
  • usingElRef
  • disableTrack
  • el.requestPictureInPicture().then(resolve).catch
  • document!.exitPictureInPicture().then(resolve).catch
  • watchEffect (from vue)
  • toValue (from vue)
  • Array.isArray
  • isObject (from @vueuse/shared)
  • el.querySelectorAll('source').forEach
  • e.remove
  • sources.forEach
  • document.createElement
  • source.setAttribute
  • useEventListener (from ../useEventListener)
  • el.appendChild
  • el.load
  • watch (from vue)
  • el.querySelectorAll('track').forEach
  • textTracks.forEach
  • watchIgnorable (from @vueuse/shared)
  • el.play().catch
  • playbackErrorEvent.trigger
  • el.pause
  • ignoreCurrentTimeUpdates
  • timeRangeToArray
  • ignorePlayingUpdates
  • stop
  • tracksToArray
  • tryOnScopeDispose (from @vueuse/shared)
  • listeners.forEach
  • listener

Internal Comments:

// Events (x4)
/**
   * Disables the specified track. If no track is specified then
   * all tracks will be disabled
   *
   * @param track The id of the track to disable
   */ (x2)
/**
   * Enables the specified track and disables the
   * other tracks unless otherwise specified
   *
   * @param track The track of the id of the track to enable
   * @param disableTracks Disable all other tracks
   */ (x2)
/**
   * Toggle picture in picture mode for the player.
   */ (x2)
/**
   * This will automatically inject sources to the media element. The sources will be
   * appended as children to the media element as `<source>` elements.
   */ (x3)
// Merge sources into an array
// Clear the sources (x6)
// Add new sources (x4)
// Finally, load the new sources. (x4)
/**
   * Apply composable state to the element, also when element is changed
   */ (x3)
/**
   * Load Tracks
   */ (x3)
/**
     * The MediaAPI provides an API for adding text tracks, but they don't currently
     * have an API for removing text tracks, so instead we will just create and remove
     * the tracks manually using the HTML api.
     */ (x6)
/**
   * This will allow us to update the current time from the timeupdate event
   * without setting the medias current position, but if the user changes the
   * current time via the ref, then the media will seek.
   *
   * If we did not use an ignorable watch, then the current time update from
   * the timeupdate event would cause the media to stutter.
   */ (x2)
/**
   * Using an ignorable watch so we can control the play state using a ref and not
   * a function
   */ (x2)
/**
   * The following listeners need to listen to a nested
   * object on the target, so we will have to use a nested
   * watch and manually remove the listeners
   */ (x2)
// Remove text track listeners (x3)
// Volume (x2)
// Tracks (x2)
// Picture in Picture (x2)

Code
export function useMediaControls(target: MaybeRef<HTMLMediaElement | null | undefined>, options: UseMediaControlsOptions = {}): UseMediaControlsReturn {
  target = toRef(target)
  options = {
    ...defaultOptions,
    ...options,
  }

  const {
    document = defaultDocument,
  } = options

  const listenerOptions = { passive: true }

  const currentTime = shallowRef(0)
  const duration = shallowRef(0)
  const seeking = shallowRef(false)
  const volume = shallowRef(1)
  const waiting = shallowRef(false)
  const ended = shallowRef(false)
  const playing = shallowRef(false)
  const rate = shallowRef(1)
  const stalled = shallowRef(false)
  const buffered = shallowRef<[number, number][]>([])
  const tracks = shallowRef<UseMediaTextTrack[]>([])
  const selectedTrack = shallowRef<number>(-1)
  const isPictureInPicture = shallowRef(false)
  const muted = shallowRef(false)

  const supportsPictureInPicture = Boolean(document && 'pictureInPictureEnabled' in document)

  // Events
  const sourceErrorEvent = createEventHook<Event>()
  const playbackErrorEvent = createEventHook<Event>()

  /**
   * Disables the specified track. If no track is specified then
   * all tracks will be disabled
   *
   * @param track The id of the track to disable
   */
  const disableTrack = (track?: number | UseMediaTextTrack) => {
    usingElRef<HTMLMediaElement>(target, (el) => {
      if (track) {
        const id = typeof track === 'number' ? track : track.id
        el.textTracks[id].mode = 'disabled'
      }
      else {
        for (let i = 0; i < el.textTracks.length; ++i)
          el.textTracks[i].mode = 'disabled'
      }

      selectedTrack.value = -1
    })
  }

  /**
   * Enables the specified track and disables the
   * other tracks unless otherwise specified
   *
   * @param track The track of the id of the track to enable
   * @param disableTracks Disable all other tracks
   */
  const enableTrack = (track: number | UseMediaTextTrack, disableTracks = true) => {
    usingElRef<HTMLMediaElement>(target, (el) => {
      const id = typeof track === 'number' ? track : track.id

      if (disableTracks)
        disableTrack()

      el.textTracks[id].mode = 'showing'
      selectedTrack.value = id
    })
  }
  /**
   * Toggle picture in picture mode for the player.
   */
  const togglePictureInPicture = () => {
    return new Promise<PictureInPictureWindow | void>((resolve, reject) => {
      usingElRef<HTMLVideoElement>(target, async (el) => {
        if (supportsPictureInPicture) {
          if (!isPictureInPicture.value) {
            el.requestPictureInPicture().then(resolve).catch(reject)
          }
          else {
            document!.exitPictureInPicture().then(resolve).catch(reject)
          }
        }
      })
    })
  }

  /**
   * This will automatically inject sources to the media element. The sources will be
   * appended as children to the media element as `<source>` elements.
   */
  watchEffect(() => {
    if (!document)
      return

    const el = toValue(target)
    if (!el)
      return

    const src = toValue(options.src)
    let sources: UseMediaSource[] = []

    if (!src)
      return

    // Merge sources into an array
    if (typeof src === 'string')
      sources = [{ src }]
    else if (Array.isArray(src))
      sources = src
    else if (isObject(src))
      sources = [src]

    // Clear the sources
    el.querySelectorAll('source').forEach((e) => {
      e.remove()
    })

    // Add new sources
    sources.forEach(({ src, type, media }) => {
      const source = document.createElement('source')

      source.setAttribute('src', src)
      source.setAttribute('type', type || '')
      source.setAttribute('media', media || '')

      useEventListener(source, 'error', sourceErrorEvent.trigger, listenerOptions)

      el.appendChild(source)
    })

    // Finally, load the new sources.
    el.load()
  })

  /**
   * Apply composable state to the element, also when element is changed
   */
  watch([target, volume], () => {
    const el = toValue(target)
    if (!el)
      return

    el.volume = volume.value
  })

  watch([target, muted], () => {
    const el = toValue(target)
    if (!el)
      return

    el.muted = muted.value
  })

  watch([target, rate], () => {
    const el = toValue(target)
    if (!el)
      return

    el.playbackRate = rate.value
  })

  /**
   * Load Tracks
   */
  watchEffect(() => {
    if (!document)
      return

    const textTracks = toValue(options.tracks)
    const el = toValue(target)

    if (!textTracks || !textTracks.length || !el)
      return

    /**
     * The MediaAPI provides an API for adding text tracks, but they don't currently
     * have an API for removing text tracks, so instead we will just create and remove
     * the tracks manually using the HTML api.
     */
    el.querySelectorAll('track').forEach(e => e.remove())

    textTracks.forEach(({ default: isDefault, kind, label, src, srcLang }, i) => {
      const track = document.createElement('track')

      track.default = isDefault || false
      track.kind = kind
      track.label = label
      track.src = src
      track.srclang = srcLang

      if (track.default)
        selectedTrack.value = i

      el.appendChild(track)
    })
  })

  /**
   * This will allow us to update the current time from the timeupdate event
   * without setting the medias current position, but if the user changes the
   * current time via the ref, then the media will seek.
   *
   * If we did not use an ignorable watch, then the current time update from
   * the timeupdate event would cause the media to stutter.
   */
  const { ignoreUpdates: ignoreCurrentTimeUpdates } = watchIgnorable(currentTime, (time) => {
    const el = toValue(target)
    if (!el)
      return

    el.currentTime = time
  })

  /**
   * Using an ignorable watch so we can control the play state using a ref and not
   * a function
   */
  const { ignoreUpdates: ignorePlayingUpdates } = watchIgnorable(playing, (isPlaying) => {
    const el = toValue(target)
    if (!el)
      return

    if (isPlaying) {
      el.play().catch((e) => {
        playbackErrorEvent.trigger(e)
        throw e
      })
    }
    else {
      el.pause()
    }
  })

  useEventListener(
    target,
    'timeupdate',
    () => ignoreCurrentTimeUpdates(() => currentTime.value = (toValue(target))!.currentTime),
    listenerOptions,
  )
  useEventListener(
    target,
    'durationchange',
    () => duration.value = (toValue(target))!.duration,
    listenerOptions,
  )
  useEventListener(
    target,
    'progress',
    () => buffered.value = timeRangeToArray((toValue(target))!.buffered),
    listenerOptions,
  )
  useEventListener(
    target,
    'seeking',
    () => seeking.value = true,
    listenerOptions,
  )
  useEventListener(
    target,
    'seeked',
    () => seeking.value = false,
    listenerOptions,
  )
  useEventListener(
    target,
    ['waiting', 'loadstart'],
    () => {
      waiting.value = true
      ignorePlayingUpdates(() => playing.value = false)
    },
    listenerOptions,
  )
  useEventListener(
    target,
    'loadeddata',
    () => waiting.value = false,
    listenerOptions,
  )
  useEventListener(
    target,
    'playing',
    () => {
      waiting.value = false
      ended.value = false
      ignorePlayingUpdates(() => playing.value = true)
    },
    listenerOptions,
  )
  useEventListener(
    target,
    'ratechange',
    () => rate.value = (toValue(target))!.playbackRate,
    listenerOptions,
  )
  useEventListener(
    target,
    'stalled',
    () => stalled.value = true,
    listenerOptions,
  )
  useEventListener(
    target,
    'ended',
    () => ended.value = true,
    listenerOptions,
  )
  useEventListener(
    target,
    'pause',
    () => ignorePlayingUpdates(() => playing.value = false),
    listenerOptions,
  )
  useEventListener(
    target,
    'play',
    () => ignorePlayingUpdates(() => playing.value = true),
    listenerOptions,
  )
  useEventListener(
    target,
    'enterpictureinpicture',
    () => isPictureInPicture.value = true,
    listenerOptions,
  )
  useEventListener(
    target,
    'leavepictureinpicture',
    () => isPictureInPicture.value = false,
    listenerOptions,
  )
  useEventListener(
    target,
    'volumechange',
    () => {
      const el = toValue(target)
      if (!el)
        return

      volume.value = el.volume
      muted.value = el.muted
    },
    listenerOptions,
  )

  /**
   * The following listeners need to listen to a nested
   * object on the target, so we will have to use a nested
   * watch and manually remove the listeners
   */
  const listeners: Fn[] = []

  const stop = watch([target], () => {
    const el = toValue(target)
    if (!el)
      return

    stop()

    listeners[0] = useEventListener(el.textTracks, 'addtrack', () => tracks.value = tracksToArray(el.textTracks), listenerOptions)
    listeners[1] = useEventListener(el.textTracks, 'removetrack', () => tracks.value = tracksToArray(el.textTracks), listenerOptions)
    listeners[2] = useEventListener(el.textTracks, 'change', () => tracks.value = tracksToArray(el.textTracks), listenerOptions)
  })

  // Remove text track listeners
  tryOnScopeDispose(() => listeners.forEach(listener => listener()))

  return {
    currentTime,
    duration,
    waiting,
    seeking,
    ended,
    stalled,
    buffered,
    playing,
    rate,

    // Volume
    volume,
    muted,

    // Tracks
    tracks,
    selectedTrack,
    enableTrack,
    disableTrack,

    // Picture in Picture
    supportsPictureInPicture,
    togglePictureInPicture,
    isPictureInPicture,

    // Events
    onSourceError: sourceErrorEvent.on,
    onPlaybackError: playbackErrorEvent.on,
  }
}

usingElRef(source: MaybeRefOrGetter<any>, cb: (el: T) => void): void

Automatically check if the ref exists and if it does run the cb fn

Raw JSDoc
/**
 * Automatically check if the ref exists and if it does run the cb fn
 */

Calls:

  • toValue (from vue)
  • cb
Code
function usingElRef<T = any>(source: MaybeRefOrGetter<any>, cb: (el: T) => void) {
  if (toValue(source))
    cb(toValue(source))
}

timeRangeToArray(timeRanges: TimeRanges): [number, number][]

Converts a TimeRange object to an array

Raw JSDoc
/**
 * Converts a TimeRange object to an array
 */

Calls:

  • timeRanges.start
  • timeRanges.end
Code
function timeRangeToArray(timeRanges: TimeRanges) {
  let ranges: [number, number][] = []

  for (let i = 0; i < timeRanges.length; ++i)
    ranges = [...ranges, [timeRanges.start(i), timeRanges.end(i)]]

  return ranges
}

tracksToArray(tracks: TextTrackList): UseMediaTextTrack[]

Converts a TextTrackList object to an array of UseMediaTextTrack

Raw JSDoc
/**
 * Converts a TextTrackList object to an array of `UseMediaTextTrack`
 */

Calls:

  • Array.from(tracks) .map
Code
function tracksToArray(tracks: TextTrackList): UseMediaTextTrack[] {
  return Array.from(tracks)
    .map(({ label, kind, language, mode, activeCues, cues, inBandMetadataTrackDispatchType }, id) => ({ id, label, kind, language, mode, activeCues, cues, inBandMetadataTrackDispatchType }))
}

Internal helpers

Declared inside another function in this file.

disableTrack(track: number | UseMediaTextTrack): void

Parameters:

  • track number | UseMediaTextTrack

Returns: void

Calls:

  • usingElRef
Code
(track?: number | UseMediaTextTrack) => {
    usingElRef<HTMLMediaElement>(target, (el) => {
      if (track) {
        const id = typeof track === 'number' ? track : track.id
        el.textTracks[id].mode = 'disabled'
      }
      else {
        for (let i = 0; i < el.textTracks.length; ++i)
          el.textTracks[i].mode = 'disabled'
      }

      selectedTrack.value = -1
    })
  }

enableTrack(track: number | UseMediaTextTrack, disableTracks: boolean): void

Parameters:

  • track number | UseMediaTextTrack
  • disableTracks boolean

Returns: void

Calls:

  • usingElRef
  • disableTrack
Code
(track: number | UseMediaTextTrack, disableTracks = true) => {
    usingElRef<HTMLMediaElement>(target, (el) => {
      const id = typeof track === 'number' ? track : track.id

      if (disableTracks)
        disableTrack()

      el.textTracks[id].mode = 'showing'
      selectedTrack.value = id
    })
  }

togglePictureInPicture(): Promise<void | PictureInPictureWindow>

Returns: Promise<void | PictureInPictureWindow>

Calls:

  • usingElRef
  • el.requestPictureInPicture().then(resolve).catch
  • document!.exitPictureInPicture().then(resolve).catch
Code
() => {
    return new Promise<PictureInPictureWindow | void>((resolve, reject) => {
      usingElRef<HTMLVideoElement>(target, async (el) => {
        if (supportsPictureInPicture) {
          if (!isPictureInPicture.value) {
            el.requestPictureInPicture().then(resolve).catch(reject)
          }
          else {
            document!.exitPictureInPicture().then(resolve).catch(reject)
          }
        }
      })
    })
  }

Interfaces

UseMediaSource

Interface Code
export interface UseMediaSource {
  /**
   * The source url for the media
   */
  src: string

  /**
   * The media codec type
   */
  type?: string

  /**
   * Specifies the media query for the resource's intended media.
   */
  media?: string
}

Properties

Name Type Optional Description
src string not shown
type string not shown
media string not shown

UseMediaTextTrackSource

Interface Code
export interface UseMediaTextTrackSource {
  /**
   * Indicates that the track should be enabled unless the user's preferences indicate
   * that another track is more appropriate
   */
  default?: boolean

  /**
   * How the text track is meant to be used. If omitted the default kind is subtitles.
   */
  kind: TextTrackKind

  /**
   * A user-readable title of the text track which is used by the browser
   * when listing available text tracks.
   */
  label: string

  /**
   * Address of the track (.vtt file). Must be a valid URL. This attribute
   * must be specified and its URL value must have the same origin as the document
   */
  src: string

  /**
   * Language of the track text data. It must be a valid BCP 47 language tag.
   * If the kind attribute is set to subtitles, then srclang must be defined.
   */
  srcLang: string
}

Properties

Name Type Optional Description
default boolean not shown
kind TextTrackKind not shown
label string not shown
src string not shown
srcLang string not shown

UseMediaControlsOptions

Interface Code
interface UseMediaControlsOptions extends ConfigurableDocument {
  /**
   * The source for the media, may either be a string, a `UseMediaSource` object, or a list
   * of `UseMediaSource` objects.
   */
  src?: MaybeRefOrGetter<string | UseMediaSource | UseMediaSource[]>

  /**
   * A list of text tracks for the media
   */
  tracks?: MaybeRefOrGetter<UseMediaTextTrackSource[]>
}

Properties

Name Type Optional Description
src MaybeRefOrGetter<string \| UseMediaSource \| UseMediaSource[]> not shown
tracks MaybeRefOrGetter<UseMediaTextTrackSource[]> not shown

UseMediaTextTrack

Interface Code
export interface UseMediaTextTrack {
  /**
   * The index of the text track
   */
  id: number

  /**
   * The text track label
   */
  label: string

  /**
   * Language of the track text data. It must be a valid BCP 47 language tag.
   * If the kind attribute is set to subtitles, then srclang must be defined.
   */
  language: string

  /**
   * Specifies the display mode of the text track, either `disabled`,
   * `hidden`, or `showing`
   */
  mode: TextTrackMode

  /**
   * How the text track is meant to be used. If omitted the default kind is subtitles.
   */
  kind: TextTrackKind

  /**
   * Indicates the track's in-band metadata track dispatch type.
   */
  inBandMetadataTrackDispatchType: string

  /**
   * A list of text track cues
   */
  cues: TextTrackCueList | null

  /**
   * A list of active text track cues
   */
  activeCues: TextTrackCueList | null
}

Properties

Name Type Optional Description
id number not shown
label string not shown
language string not shown
mode TextTrackMode not shown
kind TextTrackKind not shown
inBandMetadataTrackDispatchType string not shown
cues TextTrackCueList \| null not shown
activeCues TextTrackCueList \| null not shown

UseMediaControlsReturn

Interface Code
export interface UseMediaControlsReturn {
  currentTime: ShallowRef<number>
  duration: ShallowRef<number>
  waiting: ShallowRef<boolean>
  seeking: ShallowRef<boolean>
  ended: ShallowRef<boolean>
  stalled: ShallowRef<boolean>
  buffered: ShallowRef<[number, number][]>
  playing: ShallowRef<boolean>
  rate: ShallowRef<number>
  // Volume
  volume: ShallowRef<number>
  muted: ShallowRef<boolean>
  // Tracks
  tracks: ShallowRef<UseMediaTextTrack[]>
  selectedTrack: ShallowRef<number>
  enableTrack: (track: number | UseMediaTextTrack, disableTracks?: boolean) => void
  disableTrack: (track?: number | UseMediaTextTrack) => void
  // Picture in Picture
  supportsPictureInPicture: boolean
  togglePictureInPicture: () => Promise<PictureInPictureWindow | void>
  isPictureInPicture: ShallowRef<boolean>
  // Events
  onSourceError: EventHookOn<Event>
  onPlaybackError: EventHookOn<Event>
}

Properties

Name Type Optional Description
currentTime ShallowRef<number> not shown
duration ShallowRef<number> not shown
waiting ShallowRef<boolean> not shown
seeking ShallowRef<boolean> not shown
ended ShallowRef<boolean> not shown
stalled ShallowRef<boolean> not shown
buffered ShallowRef<[number, number][]> not shown
playing ShallowRef<boolean> not shown
rate ShallowRef<number> not shown
volume ShallowRef<number> not shown
muted ShallowRef<boolean> not shown
tracks ShallowRef<UseMediaTextTrack[]> not shown
selectedTrack ShallowRef<number> not shown
enableTrack (track: number \| UseMediaTextTrack, disableTracks?: boolean) => void not shown
disableTrack (track?: number \| UseMediaTextTrack) => void not shown
supportsPictureInPicture boolean not shown
togglePictureInPicture () => Promise<PictureInPictureWindow \| void> not shown
isPictureInPicture ShallowRef<boolean> not shown
onSourceError EventHookOn<Event> not shown
onPlaybackError EventHookOn<Event> not shown

Generated by Syntax Scribe