(function(webshim, $){ "use strict"; if(!window.console){return;} var mediaelement = webshim.mediaelement; var hasFlash = swfmini.hasFlashPlayerVersion('10.0.3'); var hasNative = webshim.support.mediaelement; var url = location.protocol+'//'+location.hostname; var tests = { urlInValid: { level: 1, test: (function(){ var reg = /^[a-z0-9\,\.\:\/\-_\;\?#\+\*\!\(\)\$\;\&\=\+]+$/i; return function(src){ return (src.src && !reg.test(src.src)); }; })(), srcTest: {poster: 1, srces: 1}, message: "URL has invalid characters. Remove any special characters and mutated vowels." }, noHeaderTest: { level: 5, test: function(src){ return src.computedContainer != 'video/youtube' && !src.ajax && !src.httpError; }, srcTest: {srces: 1}, message: "Could not run HTTP network tests (cross-domain) for all sources. Check manually." }, hasNoTypeAttribute: { level: 4, test: function(src){ return !src.declaredType && !src.typeNotRequired; }, srcTest: {srces: 1}, message: "The source element has no type attribute specified. Browser needs to download file before testing compatibility. Add a proper type attribute." }, couldNotComputeTypeDeclaredTypeAbsent: { level: 1, test: function(src){ return (!src.computedContainer && !src.declaredType); }, srcTest: {srces: 1}, message: "The source element has no type attribute specified and the extensions seems unknown. Add a proper type attribute." }, httpError: { level: 2.5, test: function(src){ if(!src.ajax || src.decode.swf.success || src.decode.native.success){ return 'not testable'; } else { return !!(src.httpError && !src.httpErrorText); } }, srcTest: {srces: 1}, message: "There was an unknown http error. Check source/URL." }, fileEncoding: { test: function(){ return 'This test does not test file encoding, framerate compatibility, moov index, encoding profiles. So there is room to fail!'; }, srcTest: {srces: 1} }, explicitHttpError: { level: 1, test: function(src){ if(!src.ajax || src.decode.swf.success || src.decode.native.success){ return 'not testable'; } else { return !!(src.httpErrorText); } }, srcTest: {srces: 1}, message: "There was a http error. Check source/URL." }, charsetInContentType: { level: 2.5, test: function(src){ if(!src.ajax || src.httpError){ return 'not testable'; } else { return src.headerType && (/charset=/i).test(src.headerType); } }, srcTest: {srces: 1}, message: "Content-Type header of media file sends charset. Remove charset information." }, explicitTypeMix: { level: 3, test: function(src){ if(src.declaredContainer && src.headerType){ return src.headerType != src.declaredType; } else { return 'not testable'; } }, srcTest: {srces: 1}, message: "Content-Type header and attribute type do not match. Set same and proper type value." }, noContentType: { level: 2.5, test: function(src){ if(src.ajax && !src.httpError){ return !(src.headerType); } else { return 'not testable'; } }, srcTest: {srces: 1}, message: "Content-Type header for media file is either empty or application/octet-stream." }, noContentLength: { level: 3, test: function(src){ if(src.ajax && !src.httpError){ return !(src.headers['Content-Length']); } else { return 'not testable'; } }, srcTest: {srces: 1}, message: "Content-Length header for media file does not send value." }, noRange: { level: 3, test: function(src){ if(src.ajax && !src.httpError){ return !(src.headers['Accept-Ranges']); } else { return 'not testable'; } }, srcTest: {srces: 1}, message: "Accept-Ranges header for media file does not send value. Make sure server supports Range requests in bytes" }, explicitNoRange: { level: 2.5, test: function(src){ if(src.ajax && !src.httpError){ return (src.headers['Accept-Ranges'] == 'none'); } else { return 'not testable'; } }, srcTest: {srces: 1}, message: "Server does not support Range requests. Make sure server supports Range requests in bytes" }, doubleEncoded: { level: 1, test: function(src){ if(src.ajax && !src.httpError){ return ((/[defalte|gzip]/i).test(src.headers['Content-Encoding'])); } else { return 'not testable'; } }, srcTest: {srces: 1}, message: "Content of media file is encoded with gzip/defalte. Make sure to not encode it. It's already encoded." }, mediaAttachment: { level: 1, test: function(src){ if(src.ajax && !src.httpError){ return (/attach/i.test(src.headers['Content-Disposition'])); } else { return 'not testable'; } }, srcTest: {srces: 1}, message: "Content-Disposition header wants media file to be downloaded, but not to be played." }, badTypeMix: { level: 1, test: function(src, infos){ var ret = false; var isPlayableHtml, isPlayableHeader; var htmlContainer = src.declaredContainer || src.computedContainer; var headerContainer = src.headerContainer; if(headerContainer && htmlContainer){ if(headerContainer != htmlContainer){ isPlayableHtml = mediaelement.swfMimeTypes.indexOf(htmlContainer) != -1; isPlayableHeader = mediaelement.swfMimeTypes.indexOf(headerContainer) != -1; if(isPlayableHtml != isPlayableHeader){ ret = true; } if(!ret && infos.element.canPlayType){ isPlayableHtml = !!infos.element.canPlayType(htmlContainer); isPlayableHeader = !!infos.element.canPlayType(headerContainer); if(isPlayableHtml != isPlayableHeader){ ret = true; } } } } else { ret = 'not testable'; } return ret; }, srcTest: {srces: 1}, message: "Content-Type header and attribute type do not match and are quite different. Set same and proper type value." }, typeMix: { level: 2.5, test: function(src, infos){ var ret = false; var isPlayableComputed, isPlayableDeclared; if(!src.headerContainer && src.declaredContainer && src.computedContainer && src.computedContainer != src.declaredContainer){ isPlayableComputed = mediaelement.swfMimeTypes.indexOf(src.computedContainer) != -1; isPlayableDeclared = mediaelement.swfMimeTypes.indexOf(src.declaredContainer) != -1; if(isPlayableComputed != isPlayableDeclared){ ret = true; } if(!ret && infos.element.canPlayType){ isPlayableComputed = !!infos.element.canPlayType(src.computedContainer); isPlayableDeclared = !!infos.element.canPlayType(src.declaredContainer); if(isPlayableComputed != isPlayableDeclared){ ret = true; } } } return ret; }, srcTest: {srces: 1}, message: "Computed type and declared type are different. Needs manual check." }, hasNoPlayableSrc: { level: 1, test: function(infos){ var hasPlayable = false; $.each(infos.srces, function(i, src){ var pluginContainer = src.declaredContainer || src.computedContainer; var nativeContainer = src.headerContainer || pluginContainer; if(mediaelement.swfMimeTypes.indexOf(pluginContainer) != -1){ hasPlayable = true; return false; } if(infos.element.canPlayType && infos.element.canPlayType(pluginContainer) && infos.element.canPlayType(nativeContainer)){ hasPlayable = true; return false; } }); return !hasPlayable; }, message: "Mediaelement has no source to be played in browser or by plugin. Use at least a video/mp4 source." }, endJump: { level: 2.5, test: function(src){ return src.decode.swf.endJump || src.decode.native.endJump; }, srcTest: {srces: 1}, message: 'src jumped to end too soon. Check negative timestamps: https://bugzilla.mozilla.org/show_bug.cgi?id=868797' }, swfTimeout: { level: 3, test: function(src){ return src.decode.swf.timeout; }, srcTest: {srces: 1}, message: 'Could not run decode tests. Maybe moovposition is on end?' }, moovPosition: { level: 2, test: function(src){ if(src.decode.swf.moovposition){ return src.decode.swf.moovposition > 300; } return false; }, srcTest: {srces: 1} }, tabletDecode: { level: 2, test: function(infos){ var hasSwfSuccess = false; var hasPlayableh264 = false; if(hasFlash){ $.each(infos.srces, function(i, src){ var swfDecode = src.decode.swf; if(('videocodecid' in swfDecode)){ hasSwfSuccess = true; } if(swfDecode.videocodecid != 'avc1' || swfDecode.avclevel > 31 || swfDecode.height * swfDecode.width > 921600){ return; } hasPlayableh264 = true; return false; }); } return (!hasSwfSuccess) ? false : !hasPlayableh264; }, message: 'Not playable on more than 25% of smartphone and more than 15% of tablet devices. In case you want to support 75% of smartphone- and 90% of tablet devices you need to provide a source encoded with H.264, High Profile (HP), Level 3.1, up to 1280 * 720.' }, allTabletDecode: { level: 3, test: function(infos){ var hasSwfSuccess = false; var hasPlayableh264 = false; if(hasFlash){ $.each(infos.srces, function(i, src){ var swfDecode = src.decode.swf; if(('videocodecid' in swfDecode)){ hasSwfSuccess = true; } if(swfDecode.videocodecid != 'avc1' || swfDecode.avcprofile > 77 || swfDecode.avclevel > 31 || swfDecode.height * swfDecode.width > 921600){ return; } hasPlayableh264 = true; return false; }); } return (!hasSwfSuccess) ? false : !hasPlayableh264; }, message: 'Not playable on more than 15% of smartphone and more than 5% of tablet devices. In case you want to support 90% of smartphone- and 99% of tablet devices you need to provide a source encoded with H.264, Main Profile (HP), Level 3.1, up to 1280 * 720.' }, smartphoneDecode: { level: 3.5, test: function(infos){ var hasSwfSuccess = false; var hasPlayableh264 = false; if(hasFlash){ $.each(infos.srces, function(i, src){ var swfDecode = src.decode.swf; if(('videocodecid' in swfDecode)){ hasSwfSuccess = true; } if(swfDecode.videocodecid != 'avc1' || swfDecode.avcprofile > 77 || swfDecode.avclevel > 30 || swfDecode.height * swfDecode.width > 345600){ return; } hasPlayableh264 = true; return false; }); } return (!hasSwfSuccess) ? false : !hasPlayableh264; }, message: 'Not playable on more than 10% of smartphones: In case you want to support 90% of smartphone- and 99% of tablet devices you need to provide a source encoded with H.264, Main Profile (HP), Level 3.1, up to 720 * 404 / 640 * 480.' }, notAllSmartphoneDecode: { level: 4, test: function(infos){ var hasSwfSuccess = false; var hasPlayableh264 = false; if(hasFlash){ $.each(infos.srces, function(i, src){ var swfDecode = src.decode.swf; if(('videocodecid' in swfDecode)){ hasSwfSuccess = true; } if(swfDecode.videocodecid != 'avc1' || swfDecode.avcprofile > 66 || swfDecode.avclevel > 30 || swfDecode.height * swfDecode.width > 307200){ return; } hasPlayableh264 = true; return false; }); } return (!hasSwfSuccess) ? false : !hasPlayableh264; }, message: 'Not playable on more than 1% of smartphones: In case you want to support 99% of all devices you need to provide a source encoded with H.264, Baseline Profile (BP), Level 3.0, up to 720 * 404 / 640 * 480. You might want to use multiple sources to satisfy quality and maximum device compatibility.' }, needsFlashInstalled: { level: 1, test: function(infos){ var flashCanPlay = false; var nativeCanPlay = false; if(!hasFlash){ $.each(infos.srces, function(i, src){ var pluginContainer = src.declaredContainer || src.computedContainer; var nativeContainer = src.headerContainer || pluginContainer; if(mediaelement.swfMimeTypes.indexOf(pluginContainer) != -1){ flashCanPlay = true; } if(infos.element.canPlayType && (pluginContainer == 'video/youtube' || (infos.element.canPlayType(pluginContainer) && infos.element.canPlayType(nativeContainer)))){ nativeCanPlay = true; return false; } }); } return flashCanPlay && !nativeCanPlay; }, message: "While media file could be played by flash plugin, Browser has no flash installed. Use at least a video/mp4 source and install flash. Or add additionally a video/webm file." }, hasNoSwfPlayableSrc: { level: 1, test: function(infos){ var hasPlayable = false; $.each(infos.srces, function(i, src){ var pluginContainer = src.declaredContainer || src.computedContainer; if(mediaelement.swfMimeTypes.indexOf(pluginContainer) != -1){ hasPlayable = true; return false; } }); return !hasPlayable; }, message: "Mediaelement has no source to be played by fallback plugin. Use at least a video/mp4 source." }, hasNoNativePlayableSrc: { level: 4, test: function(infos){ var hasPlayable = false; if(infos.element.canPlayType){ $.each(infos.srces, function(i, src){ var pluginContainer = src.declaredContainer || src.computedContainer; var nativeContainer = src.headerContainer || pluginContainer; if(pluginContainer == 'video/youtube' || (infos.element.canPlayType(pluginContainer) && infos.element.canPlayType(nativeContainer))){ hasPlayable = true; return false; } }); } return !hasPlayable; }, message: "Mediaelement has no source to be played native. Use at least a video/mp4 and a video/webm source." }, misLeadingAttrMode: { level: 2, test: function(infos){ return (infos.srces.length > 1 && infos.srces[0].attrMode); }, message: "Mediaelement has a src attribute and some source child elements. Only src attribute is used." }, emptySrc: { level: 2, test: function(src){ return src.src && !src.attrSrc; }, srcTest: {poster: 1, srces: 1}, message: "The src or poster attribute is an empty string, which is not allowed." } }; function runMediaTest(src, container, provider, infos){ var timeoutTimer, playTimer; var promise = $.Deferred(); var $container = $('#wsmediatestcontainer'); var $element = $('<div />').css({width: 320, height: 120, float: 'left'}); var $media = $(document.createElement(infos.nodeName)) .attr({ src: src.src, 'data-type': container, 'controls': 'controls', preload: 'none' }) ; var resolvePromise = function(){ $media.pause(); setTimeout(function(){ $element.remove(); if(!$('video, audio', $container).length){ $container.remove(); } }, 9); setTimeout(function(){ promise.resolve(); }, 99); }; var runEnded = function(e){ var duration = $media.prop('duration'); var currentTime = $media.prop('currentTime'); if(duration && duration > 5){ if(currentTime > 0 && currentTime < 5){ resolvePromise(); } else if(e.type == 'ended' || currentTime >= duration -1){ src.decode[provider].endJump = true; resolvePromise(); } } else { resolvePromise(); } }; var resolve = function(e){ clearTimeout(timeoutTimer); if(e){ if(e.type == 'loadedmetadata'){ if(provider == 'swf'){ try { src.decode[provider] = $media.getShadowElement().find('object, embed')[0].api_get('meta'); } catch(e){} } if(!src.decode[provider] || $.isEmptyObject(src.decode[provider])){ src.decode[provider] = { duration: $media.prop('duration'), height: $media.prop('videoHeight'), width: $media.prop('videoWidth') //todo at test for seekable //,seekable: ($media.prop('seekable') || []).length }; } src.decode[provider].success = true; } else { src.decode[provider] = { error: $media.prop('error'), mediaError: $media.data('mediaerror'), success: false }; } } else { src.decode[provider] = { success: false, timeout: true }; } setTimeout(function(){ $media.play(); }, 9); $media.on('ended timeupdate', runEnded); clearTimeout(playTimer); setTimeout(resolvePromise, 300); }; if(!$container.length){ $container = $('<div id="wsmediatestcontainer" />') .css({position: 'fixed', top: 0, left: 0, right: 0, padding: 10, zIndex: 9999999999}) .prependTo('body') ; } $media .on('mediaerror loadedmetadata', resolve) .appendTo($element) ; if(provider == 'native'){ $media.on('error', resolve); } $element.appendTo($container); timeoutTimer = setTimeout(resolve, 40000); playTimer = setTimeout(function(){ $media.prop('muted', true); $media.play(); }, 200); $media.mediaLoad(); return promise; } function runDecodeTest(src, infos){ var promises = []; var type = src.declaredContainer || src.computedContainer || src.headerContainer || ''; var preferFlash = webshim.cfg.mediaelement.preferFlash; if(hasNative && infos.element.canPlayType(type)){ webshim.cfg.mediaelement.preferFlash = false; promises.push(runMediaTest(src, type, 'native', infos)); } else { src.decode.native = {success: false, notsupported: true}; } if(hasFlash && !(/youtube|rtmp/i.test(type)) && mediaelement.swfMimeTypes.indexOf(type) != -1){ webshim.cfg.mediaelement.preferFlash = true; promises.push(runMediaTest(src, type, 'swf', infos)); } else { src.decode.swf = {success: false, notsupported: type != 'video/youtube'}; } webshim.cfg.mediaelement.preferFlash = preferFlash; return $.when.apply($, promises); } var runningDecodeTests = 0; var decodeObj = {}; function runDeferredeDcodeTest(src, infos){ var promise = $.Deferred(); var onRun = function(){ if(!runningDecodeTests){ runningDecodeTests++; $(decodeObj).off('finish', onRun); runDecodeTest(src, infos).always(function(){ promise.resolve(); runningDecodeTests--; $(decodeObj).trigger('finish'); }); } }; if(runningDecodeTests){ $(decodeObj).on('finish', onRun); } else { onRun(); } src.decode.promise = promise.promise(); } function getSrcInfo(elem, infos){ var ajax; var src = { src: $.prop(elem, 'src'), attrSrc: $.trim($.attr(elem, 'src')), declaredType: $.attr(elem, 'type') || $(elem).attr('data-type') || '', errors: {}, decode: { native: {}, swf: {} } }; src.declaredContainer = src.declaredType.split(';')[0].trim(); try { src.computedContainer = mediaelement.getTypeForSrc( src.src, infos.nodeName); } catch(e){ src.computedContainer = ''; } if(!src.src.indexOf(url)){ try { src.headerType = ''; src.headers = {}; ajax = $.ajax({ url: src.src, type: 'head', success: function(){ src.headerType = ajax.getResponseHeader('Content-Type') || ''; if((/^\s*application\/octet\-stream\s*$/i).test(src.headerType)){ src.headerType = ''; src.errors.octetStream = 'octetStream'; } src.headerContainer = $.trim(src.headerType.split(';')[0]); ['Location', 'Content-Type', 'Content-Length', 'Accept-Ranges', 'Content-Disposition', 'Content-Encoding'].forEach(function(name){ src.headers[name] = ajax.getResponseHeader(name) || ''; }); }, error: function(xhr, status, statusText){ src.httpError = status; src.httpErrorText = statusText; } }); src.ajax = ajax; } catch(e){} } else { src.cors = true; } runDeferredeDcodeTest(src, infos); return src; } function resolveSrces(infos){ var src; var srces = []; var ajaxes = []; var $sources = $('source', infos.element); var promises = []; var mainPromise = $.Deferred(); var i = 0; var resolve = function(){ i++; if(i > 1){ mainPromise.resolve(); } }; if($.prop(infos.element, 'src')){ src = getSrcInfo(infos.element, infos); src.attrMode = true; src.typeNotRequired = true; srces.push(src); } $sources.each(function(i){ var src = getSrcInfo(this, infos); src.typeNotRequired = !!(i && i >= $sources.length - 1); srces.push(src); if(src.ajax){ ajaxes.push(src.ajax); } if(src.decode.promise){ promises.push(src.decode.promise); } }); infos.srces = srces; $.when.apply($, promises).always(resolve); $.when.apply($, ajaxes).done(resolve).fail(function(){ setTimeout(resolve, 200); }); return mainPromise.promise(); } function runTests(infos){ $.each(tests, function(name, obj){ var localMessage; var failed = false; var message = obj.message || name; if(obj.srcTest){ if(obj.srcTest.poster){ localMessage = obj.test(infos.poster, infos); if(localMessage){ if(typeof localMessage == 'string'){ infos.poster.errors[name] = localMessage; } else { infos.poster.errors[name] = message; failed = true; } } } if(obj.srcTest.srces){ infos.srces.forEach(function(src){ localMessage = obj.test(src, infos); if(localMessage){ if(typeof localMessage == 'string'){ src.errors[name] = localMessage; } else { src.errors[name] = message; failed = true; } } }); } } else { failed = obj.test(infos); } if(failed){ infos.errors.push({ message: message, level: obj.level, name: name }); } }); infos.errors.sort(function(a, b){ return a.level > b.level; }); console.log('---- Media Test Start ----'); console.log('Testing results for mediaelement network + markup debugger. For detailed information expand the following object:', infos); if(infos.errors.length){ if(infos.errors[0].level < 3){ console.log('Found '+ infos.errors.length + ' errors/warnings with at least 1 critical issue.'); } else if(infos.errors[0].level < 4) { console.log('Found '+ infos.errors.length + ' errors/warnings.'); } else { console.log('Found '+ infos.errors.length + ' warnings but no critical issue.'); } infos.errors.forEach(function(error){ var type = 'log'; if(console.error && console.warn){ if(error.level < 3){ type = 'error'; } else if(error.level < 4){ type = 'warn'; } } console[type](error.message, 'priority level: '+ error.level, error.name); }); } else { console.log('Congratulations: No errors found for video.'); } console.log('---- Media Test End ----'); console.log('----'); } function getMediaInfo(elem){ var infos = { element: elem, nodeName: elem.nodeName.toLowerCase(), errors: [], poster: { src: $.prop(elem, 'poster'), attrSrc: $.trim($.attr(elem, 'poster')), errors: {} }, mediaError: $.prop(elem, 'error'), wsError: $(elem).data('mediaerror') }; var promise = resolveSrces(infos); var initTests = function(){ runTests(infos); }; promise.always(initTests); } var timedMediaInfo = function(i){ var elem = this; setTimeout(function(){ getMediaInfo(elem); }, i * 100); }; console.log('Running mediaelement debugger. Only run these tests in development never in production. set webshim.setOptions("debug", false); to remove. Debugger only tests media on same domain and does not test all file encoding issues. So there is still room to fail!'); if(webshim.cfg.extendNative){ console.log('mediaelement debugger does not detect all problems with extendNative set to true. Please set webshim.setOptions("extendNative", false);'); } webshim.addReady(function(context, $insertedElement){ $('video, audio', context) .add($insertedElement.filter('video, audio')) .each(timedMediaInfo) ; }); webshim.mediaelement.getMediaInfo = getMediaInfo; })(webshim, webshim.$);