Skip to content

Browser version #37

@jimmywarting

Description

@jimmywarting

Want to share a browser version that uses the fetch readable stream api and MSE.
it uses a fraction of what a browser bundle of what this package uses... And it has no dependencies

Code
{
  const mozChunked = 'moz-chunked-arraybuffer';
  const supportChunked = (b => {
    try {
      return (b.responseType = mozChunked), b.responseType == mozChunked
    } catch (c) {
      return !1
    }
  })(new XMLHttpRequest);
  const supportsFetch = typeof Response !== 'undefined' &&
                        Response.prototype.hasOwnProperty('body')

  const trim = (arr, b = arr.length) => {
    for (;~--b && arr[b] === 0;);
    return arr.slice(0, b+1)
  }

  const parseMeta = str => {
    const pieces = str.split(';')
    const rtn = {}

    for (let peace of pieces) {
      peace = peace.trim();
      if (peace.length > 0) {
        const delimiter = /\=(['"])/.exec(peace)
        const name = peace.substring(0, delimiter.index)
        const value = peace.substring(delimiter.index + 2, peace.length - 1)
        rtn[name.trim()] = value.trim()
      }
    }
    return rtn
  }

  const attach = (req, player) => {
    if (!(req instanceof Request))
      req = new Request(req)

    req.headers.set('icy-metadata', '1')

    const decoder = new TextDecoder
    const ms = new MediaSource()
    const track = player.addTextTrack('metadata')
    const {cues} = track
    const segments = []

    /**********************
     0 = reading raw data
     1 = reading byteRange
     2 = reading meta data
    **********************/
    let state = 0
    let metaint;
    let byteLeftToRead = 0;
    let sourceBuffer;

    player.src = URL.createObjectURL(ms)
    player.controls = true

    const doAppend = () => {
      if (sourceBuffer.updating || !segments.length)
        return

      let chunk = segments.shift()

      // large buffer needs to be sliced so
      // metadata is not feed into the sourceBuffer
      if (chunk.length > byteLeftToRead) {
        const b = chunk.slice(0, byteLeftToRead)
        segments.unshift(chunk.slice(b.length))
        chunk = b
      }

      if (state === 2) {
        // small buffer needs to fill the remaining metadata
        if (chunk.length < byteLeftToRead) {
          if (segments.length) {
            const a = chunk
            const b = segments.shift()
            const c = new Int8Array(a.length + b.length)
            c.set(a)
            c.set(b, a.length)
            segments.unshift(c)
          }
          return
        }
      }

      byteLeftToRead -= chunk.length

      if (state === 0) {
        // Reading raw metaData
        if (byteLeftToRead === 0) {
          state = 1
          byteLeftToRead = 1
        }
        sourceBuffer.appendBuffer(chunk)
      } else if (state === 1) {
        byteLeftToRead = chunk[0] * 16
        if (byteLeftToRead > 0) {
          state = 2
        } else {
          state = 0
          byteLeftToRead = metaint
        }
      } else {
        chunk = trim(chunk)
        const {StreamTitle} = parseMeta(decoder.decode(chunk))
        const end = Number.MAX_SAFE_INTEGER
        let start = 0
        if (cues.length !== 0) {
          const last = cues[cues.length - 1]
          start = last.endTime = sourceBuffer.buffered.end(0)
        }

        track.addCue(new VTTCue(start, end, StreamTitle))
        state = 0
        byteLeftToRead = metaint
      }
    }

    req = supportsFetch ? fetch(req).then(res => {
      // Use fetch streaming technique
      const reader = res.body.getReader()
      const pump = () => reader.read().then(chunk => {
        window.e = new Blob([window.e || '', chunk.value], {type: 'application/wergfwe'})
        if (chunk.value) segments.push(chunk.value)
        doAppend()
        return chunk.done ? undefined : pump()
      })

      byteLeftToRead = metaint = ~~res.headers.get('icy-metaint') || 8192

      return pump
    }) : supportChunked ? new Promise(rs => {
      // Use Firefox XHR's moz-chunked-arraybuffer streaming technique
      const xhr = new XMLHttpRequest
      xhr.open('get', req.url)
      xhr.setRequestHeader('icy-metadata', '1')
      xhr.responseType = mozChunked
      xhr.onreadystatechange = () => {
        if (xhr.readyState === xhr.HEADERS_RECEIVED) {
          byteLeftToRead = metaint = ~~xhr.getResponseHeader('icy-metaint') || 8192
        }
        rs(() => {})
      }
      xhr.onprogress = () => {
        const bytes = new Uint8Array(xhr.response).slice()
        segments.push(bytes)
      }
      xhr.send()
    }) : Promise.reject(new Error('unable to stream audio'))

    ms.addEventListener('sourceopen', async evt => {
      sourceBuffer = evt.target.addSourceBuffer('audio/aac')
      sourceBuffer.mode = 'sequence'
      req.then(pump => pump())
      window.sb = sourceBuffer
      sourceBuffer.addEventListener('updateend', () => {
        if (!sourceBuffer.updating) ms.duration = sourceBuffer.buffered.end(0)
        doAppend()
      }, false)

      player.play()
    }, false);
  }


  const play = req => ({
    in(selector) {
      let player;

      // turn string into selector
      if (typeof selector === 'string') {
        selector = document.querySelector(selector)
      }

      const isNode = selector instanceof HTMLElement
      const isMedia = selector instanceof HTMLAudioElement ||
                      selector instanceof HTMLVideoElement

      if (isNode) {
        if (isMedia) {
          player = selector
        } else {
          player = new Audio
          player.controls = true
          selector.appendChild(player)
        }
      } else {
        player = new Audio
      }

      attach(req, player)
      return player
    }
  })

  window.play = play
}

Api

play(url).in(querySelector) // string or node elm (if it's not a video/audio element it will append a audio elm)
play(url).in(window) // plays without appending to body
const player = play(url).in('body') // append a new audio elm to body with controls set to true

// From here just use the media api.
// this will give you the current title of the auido that are being played
player.textTracks[0].oncuechange = evt => console.log(evt.target.activeCues[0].text)

update: included streaming for firefox alos, but it don't support the codec that i pass in addSourceBuffer... solution?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions