Skip to content

Commit

Permalink
Merge pull request #10 from nypublicradio/mime-types
Browse files Browse the repository at this point in the history
Use mime types for playability checks on connections instead of file extensions, and if no mime type is found try it on the connection anyway
  • Loading branch information
jkeen committed Sep 25, 2016
2 parents 18e9e79 + 63ffcb2 commit 244c309
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 27 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ The files created by the blueprint should walk you through what you need to impl
// Do any global setup needed for your third party library.
},

canPlayExtension(/* extension */) {
// check if connection can play file with this extension
canPlayMimeType(/* extension */) {
// check if connection can play file with this mime type
return true;
},

Expand All @@ -191,7 +191,7 @@ The files created by the blueprint should walk you through what you need to impl
});
```

`canPlayExtension` and `canUseConnection` are called when `hifi` is looking for connections to try with a url. Give your best guess here. For instance, our built-in HLS.js library won't work on mobile, so `canUseConnection` returns false on a mobile device and true on a desktop browser. Similary, HLS only plays `.m3u8` files, so we just check for that extension in `canPlayExtension`.
`canPlayMimeType` and `canUseConnection` are called when `hifi` is looking for connections to try with a url. Give your best guess here. For instance, our built-in HLS.js library won't work on mobile, so `canUseConnection` returns false on a mobile device and true on a desktop browser. Similary, HLS only plays `application/vnd.apple.mpegurl` files, so we just check for that extension in `canPlayMimeType`.

##### Implement methods to bridge communication between hifi and your third party sound.

Expand Down
38 changes: 29 additions & 9 deletions addon/hifi-connections/base.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Ember from 'ember';
import { getMimeType } from 'ember-hifi/utils/mime-types';

const {
assert,
Expand All @@ -11,23 +12,42 @@ let ClassMethods = Ember.Mixin.create({
},

canPlay(url) {
let urlExtension = url.split('.').pop().split('?').shift().split('#').shift();
return this.canUseConnection(url) && this.canPlayExtension(urlExtension);
let usablePlatform = this.canUseConnection(url);
if (!usablePlatform) {
return false;
}
if (typeof url === 'string') {
let mimeType = getMimeType(url);
if (!mimeType) {
console.warn(`Could not determine mime type for ${url}`);
console.warn('Attempting to play urls with an unknown mime type can be bad for performance. See http://www.blahbalaha.com for more.');
return true;
}
else {
return this.canPlayMimeType(mimeType);
}
}
else if (url.mimeType) {
return this.canPlayMimeType(url.mimeType);
}
else {
throw new Error('URL must be a string or object with a mimeType property');
}
},

canUseConnection() {
return true;
},

canPlayExtension(extension) {
let whiteList = this.extensionWhiteList;
let blackList = this.extensionBlackList;
canPlayMimeType(mimeType) {
let mimeTypeWhiteList = this.acceptMimeTypes;
let mimeTypeBlackList = this.rejectMimeTypes;

if (whiteList) {
return Ember.A(whiteList).contains(extension);
if (mimeTypeWhiteList) {
return Ember.A(mimeTypeWhiteList).contains(mimeType);
}
else if (blackList){
return !Ember.A(blackList).contains(extension);
else if (mimeTypeBlackList){
return !Ember.A(mimeTypeBlackList).contains(mimeType);
}
else {
return true; // assume true
Expand Down
2 changes: 1 addition & 1 deletion addon/hifi-connections/dummy-connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ let ClassMethods = Ember.Mixin.create({
setup() {},
canPlay: () => true,
canUseConnection: () => true,
canPlayExtension: () => true,
canPlayMimeType: () => true,
});

let DummyConnection = Ember.Object.extend(Ember.Evented, {
Expand Down
2 changes: 1 addition & 1 deletion addon/hifi-connections/hls.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BaseSound from './base';
import HLS from 'hls';

let ClassMethods = Ember.Mixin.create({
extensionWhiteList: ['m3u8'],
acceptMimeTypes: ['application/vnd.apple.mpegurl'],

canUseConnection(/* audioUrl */) {
// We basically never want to use this on a mobile device
Expand Down
2 changes: 1 addition & 1 deletion addon/hifi-connections/howler.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BaseSound from './base';
import { Howl } from 'howler';

let ClassMethods = Ember.Mixin.create({
extensionBlackList: ['m3u8'],
rejectMimeTypes: ['application/vnd.apple.mpegurl'],

toString() {
return 'Howler';
Expand Down
12 changes: 3 additions & 9 deletions addon/hifi-connections/native-audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@ import Ember from 'ember';
import BaseSound from './base';

let ClassMethods = Ember.Mixin.create({
canPlayExtension(extension) {
canPlayMimeType(mimeType) {
let audio = new Audio();

if (Ember.A(['m3u8', 'm3u']).contains(extension)) {
return audio.canPlayType(`audio/mpeg`) !== "";
}
else {
// it returns "probably" and "maybe". Both are worth trying. Empty is bad.
return (audio.canPlayType(`audio/${extension}`) !== "");
}
// it returns "probably" and "maybe". Both are worth trying. Empty is bad.
return (audio.canPlayType(mimeType) !== "");
},

toString() {
Expand Down
2 changes: 1 addition & 1 deletion addon/services/hifi.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ export default Service.extend(Ember.Evented, {
strategies.push({
connectionName: name,
connection: connection,
url: url
url: url.url || url
});
}
});
Expand Down
230 changes: 230 additions & 0 deletions addon/utils/mime-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// we are only concerned with a subset of all available mime types
// data and function lifted from https://github.com/jshttp/mime-db
const MIME_TYPES = {
"audio/3gpp": {
"source": "iana",
"extensions": ["3gpp"]
},
"audio/adpcm": {
"source": "apache",
"extensions": ["adp"]
},
"audio/basic": {
"source": "iana",
"compressible": false,
"extensions": ["au","snd"]
},
"audio/midi": {
"source": "apache",
"extensions": ["mid","midi","kar","rmi"]
},
"audio/mp3": {
"compressible": false,
"extensions": ["mp3"]
},
"audio/mp4": {
"source": "iana",
"compressible": false,
"extensions": ["m4a","mp4a"]
},
"audio/mpeg": {
"source": "iana",
"compressible": false,
"extensions": ["mpga","mp2","mp2a","mp3","m2a","m3a"]
},
"audio/ogg": {
"source": "iana",
"compressible": false,
"extensions": ["oga","ogg","spx"]
},
"audio/s3m": {
"source": "apache",
"extensions": ["s3m"]
},
"audio/silk": {
"source": "apache",
"extensions": ["sil"]
},
"audio/vnd.dece.audio": {
"source": "iana",
"extensions": ["uva","uvva"]
},
"audio/vnd.digital-winds": {
"source": "iana",
"extensions": ["eol"]
},
"audio/vnd.dra": {
"source": "iana",
"extensions": ["dra"]
},
"audio/vnd.dts": {
"source": "iana",
"extensions": ["dts"]
},
"audio/vnd.dts.hd": {
"source": "iana",
"extensions": ["dtshd"]
},
"audio/vnd.lucent.voice": {
"source": "iana",
"extensions": ["lvp"]
},
"audio/vnd.ms-playready.media.pya": {
"source": "iana",
"extensions": ["pya"]
},
"audio/vnd.nuera.ecelp4800": {
"source": "iana",
"extensions": ["ecelp4800"]
},
"audio/vnd.nuera.ecelp7470": {
"source": "iana",
"extensions": ["ecelp7470"]
},
"audio/vnd.nuera.ecelp9600": {
"source": "iana",
"extensions": ["ecelp9600"]
},
"audio/vnd.rip": {
"source": "iana",
"extensions": ["rip"]
},
"audio/wav": {
"compressible": false,
"extensions": ["wav"]
},
"audio/wave": {
"compressible": false,
"extensions": ["wav"]
},
"audio/webm": {
"source": "apache",
"compressible": false,
"extensions": ["weba"]
},
"audio/x-aac": {
"source": "apache",
"compressible": false,
"extensions": ["aac"]
},
"audio/x-aiff": {
"source": "apache",
"extensions": ["aif","aiff","aifc"]
},
"audio/x-caf": {
"source": "apache",
"compressible": false,
"extensions": ["caf"]
},
"audio/x-flac": {
"source": "apache",
"extensions": ["flac"]
},
"audio/x-m4a": {
"source": "nginx",
"extensions": ["m4a"]
},
"audio/x-matroska": {
"source": "apache",
"extensions": ["mka"]
},
"audio/x-mpegurl": {
"source": "apache",
"extensions": ["m3u"]
},
"audio/x-ms-wax": {
"source": "apache",
"extensions": ["wax"]
},
"audio/x-ms-wma": {
"source": "apache",
"extensions": ["wma"]
},
"audio/x-pn-realaudio": {
"source": "apache",
"extensions": ["ram","ra"]
},
"audio/x-pn-realaudio-plugin": {
"source": "apache",
"extensions": ["rmp"]
},
"audio/x-realaudio": {
"source": "nginx",
"extensions": ["ra"]
},
"audio/x-wav": {
"source": "apache",
"extensions": ["wav"]
},
"audio/xm": {
"source": "apache",
"extensions": ["xm"]
},
"application/vnd.apple.mpegurl": {
"source": "iana",
"extensions": ["m3u8"]
},
"audio/x-mpepgurl": {
"source": "apache",
"extensions": ["m3u"]
}
};

const TYPES = Object.create(null);
const EXTENSIONS = Object.create(null);

function populateMaps(extensions, types) {
// source preference (least -> most)
let preference = ['nginx', 'apache', undefined, 'iana'];

Object.keys(MIME_TYPES).forEach(function(type) {
let mime = MIME_TYPES[type];
let exts = mime.extensions;

if (!exts || !exts.length) {
return false;
}

// mime -> extensions
extensions[type] = exts;

// extension -> mime
for (let i = 0; i < exts.length; i++) {
let extension = exts[i];

if (types[extension]) {
let start = preference.indexOf(MIME_TYPES[types[extension]].source);
let end = preference.indexOf(mime.source);

if (types[extension] !== 'application/octet-stream' &&
start > end || (start === end && types[extension].substr(0, 12) === 'application/')) {
// skip the remapping
continue;
}
}

// set the extension -> mime
types[extension] = type;
}
});
}

populateMaps(EXTENSIONS, TYPES);

export function getMimeType(url) {
if (!url || typeof url !== 'string') {
return false;
}

let extension = url
.toLowerCase()
.split('.').pop()
.split('?').shift()
.split('#').shift();

if (!extension) {
return false;
}

return TYPES[extension] || false;
}
1 change: 1 addition & 0 deletions app/utils/mime-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getMimeType } from 'ember-hifi/utils/mime-types';
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ let ClassMethods = Ember.Mixin.create({
// Do any global setup needed for your third party library.
},

canPlayExtension(/* extension */) {
// check if connection can play file with this extension
canPlayMimeType(/* extension */) {
// check if connection can play file with this mime type
return true;
},

Expand Down
5 changes: 5 additions & 0 deletions tests/unit/hifi-connections/base-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ test("isPlaying gets set to false when an 'audio-stopped' event is fired", funct

done();
});

test("base sound will eagerly accept unknown mime types", function(assert) {
let unknownMimeType = "http://www.example.come/audio";
assert.equal(baseSound.constructor.canPlay(unknownMimeType), true, "defaults to true if the mime type cannot be determined");
});
Loading

0 comments on commit 244c309

Please sign in to comment.