UNPKG

render-media

Version:

Intelligently render media files in the browser

346 lines (289 loc) 8.75 kB
exports.render = render exports.append = append exports.mime = require('./lib/mime.json') var debug = require('debug')('render-media') var isAscii = require('is-ascii') var MediaElementWrapper = require('mediasource') var path = require('path') var streamToBlobURL = require('stream-to-blob-url') var videostream = require('videostream') var VIDEOSTREAM_EXTS = [ '.m4a', '.m4v', '.mp4' ] var MEDIASOURCE_VIDEO_EXTS = [ '.m4v', '.mkv', '.mp4', '.webm' ] var MEDIASOURCE_AUDIO_EXTS = [ '.m4a', '.mp3' ] var MEDIASOURCE_EXTS = [].concat( MEDIASOURCE_VIDEO_EXTS, MEDIASOURCE_AUDIO_EXTS ) var AUDIO_EXTS = [ '.aac', '.oga', '.ogg', '.wav' ] var IMAGE_EXTS = [ '.bmp', '.gif', '.jpeg', '.jpg', '.png' ] var IFRAME_EXTS = [ '.css', '.html', '.js', '.md', '.pdf', '.txt' ] // Maximum file length for which the Blob URL strategy will be attempted // See: https://github.com/feross/render-media/issues/18 var MAX_BLOB_LENGTH = 100 * 1000 * 1000 // 100 MB var MediaSource = typeof window !== 'undefined' && window.MediaSource function render (file, elem, opts, cb) { if (typeof opts === 'function') { cb = opts opts = {} } if (!opts) opts = {} if (!cb) cb = function () {} validateFile(file) parseOpts(opts) if (typeof elem === 'string') elem = document.querySelector(elem) renderMedia(file, function (tagName) { if (elem.nodeName !== tagName.toUpperCase()) { var extname = path.extname(file.name).toLowerCase() throw new Error( 'Cannot render "' + extname + '" inside a "' + elem.nodeName.toLowerCase() + '" element, expected "' + tagName + '"' ) } return elem }, opts, cb) } function append (file, rootElem, opts, cb) { if (typeof opts === 'function') { cb = opts opts = {} } if (!opts) opts = {} if (!cb) cb = function () {} validateFile(file) parseOpts(opts) if (typeof rootElem === 'string') rootElem = document.querySelector(rootElem) if (rootElem && (rootElem.nodeName === 'VIDEO' || rootElem.nodeName === 'AUDIO')) { throw new Error( 'Invalid video/audio node argument. Argument must be root element that ' + 'video/audio tag will be appended to.' ) } renderMedia(file, getElem, opts, done) function getElem (tagName) { if (tagName === 'video' || tagName === 'audio') return createMedia(tagName) else return createElem(tagName) } function createMedia (tagName) { var elem = createElem(tagName) if (opts.controls) elem.controls = true if (opts.autoplay) elem.autoplay = true rootElem.appendChild(elem) return elem } function createElem (tagName) { var elem = document.createElement(tagName) rootElem.appendChild(elem) return elem } function done (err, elem) { if (err && elem) elem.remove() cb(err, elem) } } function renderMedia (file, getElem, opts, cb) { var extname = path.extname(file.name).toLowerCase() var currentTime = 0 var elem if (MEDIASOURCE_EXTS.indexOf(extname) >= 0) { renderMediaSource() } else if (AUDIO_EXTS.indexOf(extname) >= 0) { renderAudio() } else if (IMAGE_EXTS.indexOf(extname) >= 0) { renderImage() } else if (IFRAME_EXTS.indexOf(extname) >= 0) { renderIframe() } else { tryRenderIframe() } function renderMediaSource () { var tagName = MEDIASOURCE_VIDEO_EXTS.indexOf(extname) >= 0 ? 'video' : 'audio' if (MediaSource) { if (VIDEOSTREAM_EXTS.indexOf(extname) >= 0) { useVideostream() } else { useMediaSource() } } else { useBlobURL() } function useVideostream () { debug('Use `videostream` package for ' + file.name) prepareElem() elem.addEventListener('error', fallbackToMediaSource) elem.addEventListener('loadstart', onLoadStart) elem.addEventListener('canplay', onCanPlay) videostream(file, elem) } function useMediaSource () { debug('Use MediaSource API for ' + file.name) prepareElem() elem.addEventListener('error', fallbackToBlobURL) elem.addEventListener('loadstart', onLoadStart) elem.addEventListener('canplay', onCanPlay) var wrapper = new MediaElementWrapper(elem) var writable = wrapper.createWriteStream(getCodec(file.name)) file.createReadStream().pipe(writable) if (currentTime) elem.currentTime = currentTime } function useBlobURL () { debug('Use Blob URL for ' + file.name) prepareElem() elem.addEventListener('error', fatalError) elem.addEventListener('loadstart', onLoadStart) elem.addEventListener('canplay', onCanPlay) getBlobURL(file, function (err, url) { if (err) return fatalError(err) elem.src = url if (currentTime) elem.currentTime = currentTime }) } function fallbackToMediaSource (err) { debug('videostream error: fallback to MediaSource API: %o', err.message || err) elem.removeEventListener('error', fallbackToMediaSource) elem.removeEventListener('canplay', onCanPlay) useMediaSource() } function fallbackToBlobURL (err) { debug('MediaSource API error: fallback to Blob URL: %o', err.message || err) if (typeof file.length === 'number' && file.length > MAX_BLOB_LENGTH) { debug( 'File length too large for Blob URL approach: %d (max: %d)', file.length, MAX_BLOB_LENGTH ) return fatalError(err) } elem.removeEventListener('error', fallbackToBlobURL) elem.removeEventListener('canplay', onCanPlay) useBlobURL() } function prepareElem () { if (!elem) { elem = getElem(tagName) elem.addEventListener('progress', function () { currentTime = elem.currentTime }) } } } function renderAudio () { elem = getElem('audio') getBlobURL(file, function (err, url) { if (err) return fatalError(err) elem.addEventListener('error', fatalError) elem.addEventListener('loadstart', onLoadStart) elem.addEventListener('canplay', onCanPlay) elem.src = url }) } function onLoadStart () { elem.removeEventListener('loadstart', onLoadStart) if (opts.autoplay) elem.play() } function onCanPlay () { elem.removeEventListener('canplay', onCanPlay) cb(null, elem) } function renderImage () { elem = getElem('img') getBlobURL(file, function (err, url) { if (err) return fatalError(err) elem.src = url elem.alt = file.name cb(null, elem) }) } function renderIframe () { elem = getElem('iframe') getBlobURL(file, function (err, url) { if (err) return fatalError(err) elem.src = url if (extname !== '.pdf') elem.sandbox = 'allow-forms allow-scripts' cb(null, elem) }) } function tryRenderIframe () { debug('Unknown file extension "%s" - will attempt to render into iframe', extname) var str = '' file.createReadStream({ start: 0, end: 1000 }) .setEncoding('utf8') .on('data', function (chunk) { str += chunk }) .on('end', done) .on('error', cb) function done () { if (isAscii(str)) { debug('File extension "%s" appears ascii, so will render.', extname) renderIframe() } else { debug('File extension "%s" appears non-ascii, will not render.', extname) cb(new Error('Unsupported file type "' + extname + '": Cannot append to DOM')) } } } function fatalError (err) { err.message = 'Error rendering file "' + file.name + '": ' + err.message debug(err.message) cb(err) } } function getBlobURL (file, cb) { var extname = path.extname(file.name).toLowerCase() streamToBlobURL(file.createReadStream(), exports.mime[extname], cb) } function validateFile (file) { if (file == null) { throw new Error('file cannot be null or undefined') } if (typeof file.name !== 'string') { throw new Error('missing or invalid file.name property') } if (typeof file.createReadStream !== 'function') { throw new Error('missing or invalid file.createReadStream property') } } function getCodec (name) { var extname = path.extname(name).toLowerCase() return { '.m4a': 'audio/mp4; codecs="mp4a.40.5"', '.m4v': 'video/mp4; codecs="avc1.640029, mp4a.40.5"', '.mkv': 'video/webm; codecs="avc1.640029, mp4a.40.5"', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4; codecs="avc1.640029, mp4a.40.5"', '.webm': 'video/webm; codecs="vorbis, vp8"' }[extname] } function parseOpts (opts) { if (opts.autoplay == null) opts.autoplay = true if (opts.controls == null) opts.controls = true }