Merge branch 'master' into fs-perf

This commit is contained in:
Myk Melez 2014-11-25 23:32:26 -08:00
Родитель a926538dcd 3788a1e6a1
Коммит 1706100a89
9 изменённых файлов: 560 добавлений и 68 удалений

133
index.js
Просмотреть файл

@ -36,10 +36,6 @@ var DumbPipe = {
// Functions that receive messages from the other side for active pipes.
recipients: {},
// Queue of messages to send to the other side. Retrieved by the other side
// via a "get" message.
outgoingMessages: [],
// Every time we want to make the other side retrieve messages, the hash
// of the other side's web page has to change, so we increment it.
nextHashID: 0,
@ -76,9 +72,6 @@ var DumbPipe = {
//console.log("outer recv: " + JSON.stringify(envelope));
this.receiveMessage(envelope.pipeID, envelope.message);
break;
case "get":
this.getMessages(event);
break;
case "close":
//console.log("outer recv: " + JSON.stringify(envelope));
this.closePipe(envelope.pipeID);
@ -109,12 +102,12 @@ var DumbPipe = {
// Oh my shod, that's some funky git!
var envelope = { pipeID: pipeID, message: message };
//console.log("outer send: " + JSON.stringify(envelope));
this.outgoingMessages.push(envelope);
var mozbrowser = document.getElementById("mozbrowser");
window.setZeroTimeout(function() {
mozbrowser.src = mozbrowser.src.split("#")[0] + "#" + this.nextHashID++;
}.bind(this));
try {
document.getElementById("mozbrowser").contentWindow.postMessage(envelope, "*");
} catch (e) {
console.log("Error " + e + " while sending message: " + JSON.stringify(message));
}
},
receiveMessage: function(pipeID, message, detail) {
@ -132,17 +125,6 @@ var DumbPipe = {
}.bind(this));
},
getMessages: function(event) {
try {
event.detail.returnValue = JSON.stringify(this.outgoingMessages);
} catch(ex) {
console.error("failed to stringify outgoing messages: " + ex);
} finally {
this.outgoingMessages = [];
event.detail.unblock();
}
},
closePipe: function(pipeID) {
delete this.recipients[pipeID];
}
@ -226,11 +208,7 @@ DumbPipe.registerOpener("socket", function(message, sender) {
}
socket.ondata = function(event) {
// Turn the buffer into a regular Array to traverse the mozbrowser boundary.
var array = Array.prototype.slice.call(new Uint8Array(event.data));
array.constructor = Array;
sender({ type: "data", data: array });
sender({ type: "data", data: event.data });
}
socket.ondrain = function(event) {
@ -264,3 +242,102 @@ DumbPipe.registerOpener("socket", function(message, sender) {
}
};
});
DumbPipe.registerOpener("audiorecorder", function(message, sender) {
var mediaRecorder = null;
var localAudioStream = null;
function startRecording(localStream) {
localAudioStream = localStream;
mediaRecorder = new MediaRecorder(localStream, {
mimeType: message.mimeType // 'audio/3gpp' // need to be certified app.
});
mediaRecorder.ondataavailable = function(e) {
if (e.data.size == 0) {
return;
}
var fileReader = new FileReader();
fileReader.onload = function() {
sender({ type: "data", data: fileReader.result });
};
fileReader.readAsArrayBuffer(e.data);
};
mediaRecorder.onstop = function(e) {
// Do nothing here, just relay the event.
//
// We can't close the pipe here, one reason is |onstop| is fired before |ondataavailable|,
// if close pipe here, there is no chance to deliever the recorded voice. Another reason is
// the recording might be stopped and started back and forth. So let's do the pipe
// closing on the other side instead, i.e. DirectRecord::nClose.
sender({ type: "stop" });
};
mediaRecorder.onerror = function(e) {
sender({ type: "error" });
};
mediaRecorder.onpause = function(e) {
sender({ type: "pause" });
};
mediaRecorder.onstart = function(e) {
sender({ type: "start" });
};
mediaRecorder.start();
}
return function(message) {
switch(message.type) {
case "start":
try {
if (!mediaRecorder) {
navigator.mozGetUserMedia({
audio: true
}, function(localStream) {
startRecording(localStream);
}, function(e) {
sender({ type: "error", error: e });
});
} else if (mediaRecorder.state == "paused") {
mediaRecorder.resume();
} else {
mediaRecorder.start();
}
} catch (e) {
sender({ type: "error", error: e });
}
break;
case "requestData":
try {
// An InvalidState error might be thrown.
mediaRecorder.requestData();
} catch (e) {
sender({ type: "error", error: e });
}
break;
case "pause":
try {
mediaRecorder.pause();
} catch (e) {
sender({ type: "error", error: e });
}
break;
case "stop":
try {
mediaRecorder.stop();
localAudioStream.stop();
mediaRecorder = null;
localAudioStream = null;
} catch (e) {
sender({ type: "error", error: e });
}
break;
}
};
});

Просмотреть файл

@ -291,6 +291,24 @@ var fs = (function() {
}
}
function flushAll() {
for (var fd = 0; fd < openedFiles.length; fd++) {
if (!openedFiles[fd] || !openedFiles[fd].dirty) {
continue;
}
flush(fd);
}
}
// Due to bug #227, we don't support Object::finalize(). But the Java
// filesystem implementation requires the `finalize` method to save cached
// file data if user doesn't flush or close the file explicitly. To avoid
// losing data, we flush files periodically.
setInterval(flushAll, 5000);
// Flush files when app goes into background.
window.addEventListener("pagehide", flushAll);
function list(path, cb) {
path = normalizePath(path);
if (DEBUG_FS) { console.log("fs list " + path); }

Просмотреть файл

@ -78,46 +78,27 @@ var DumbPipe = {
}
},
handleEvent: function(event) {
// To ensure we don't fill up the browser history over time, we navigate
// "back" every time the other side navigates us "forward" by changing
// the hash. This will trigger a second hashchange event; to avoid getting
// messages twice, we only get them for the second hashchange event,
// i.e. once we've returned to the hashless page, at which point a second
// call to window.history.back() will have had no effect.
//
// We only do this when we're in mozbrowser (i.e. window.parent === window),
// since window.history.back() affects the parent window otherwise.
//
if (window.parent === window) {
var hash = window.location.hash;
window.history.back();
if (window.location.hash != hash) {
return;
}
receiveMessage: function(event) {
if (event.source === window) {
return;
}
this.send({ command: "get" }, function(envelopes) {
envelopes.forEach((function(envelope) {
//console.log("inner recv: " + JSON.stringify(envelope));
window.setZeroTimeout(function() {
if (this.recipients[envelope.pipeID]) {
try {
this.recipients[envelope.pipeID](envelope.message);
} catch(ex) {
console.error(ex + "\n" + ex.stack);
}
} else {
console.warn("nonexistent pipe " + envelope.pipeID + " received message " +
JSON.stringify(envelope.message));
}
}.bind(this));
}).bind(this));
}.bind(this));
var envelope = event.data;
if (this.recipients[envelope.pipeID]) {
try {
this.recipients[envelope.pipeID](envelope.message);
} catch(ex) {
console.error(ex + "\n" + ex.stack);
}
} else {
console.warn("nonexistent pipe " + envelope.pipeID + " received message " +
JSON.stringify(envelope.message));
}
},
};
window.addEventListener("hashchange", DumbPipe.handleEvent.bind(DumbPipe), false);
window.addEventListener("message", DumbPipe.receiveMessage.bind(DumbPipe), false);
// If "mozbrowser" isn't enabled on the frame we're loaded in, then override
// the alert/prompt functions to funnel messages to the endpoint in the parent.

Просмотреть файл

@ -20,6 +20,7 @@
* midletClassName
* network_mcc
* network_mnc
* platform
* profile
* pushConn
* pushMidlet

Просмотреть файл

@ -23,7 +23,10 @@
"mobilenetwork": {
"description:": "Required to verify your phone number"
},
"browser": {}
"browser": {},
"audio-capture": {
"description": "Required to capture audio via getUserMedia"
}
},
"type": "privileged"
}

Просмотреть файл

@ -10,6 +10,7 @@ Media.ContentTypes = {
],
file: [
"audio/ogg",
"audio/x-wav",
"audio/mpeg",
"image/jpeg",
@ -68,6 +69,7 @@ Media.extToFormat = new Map([
]);
Media.contentTypeToFormat = new Map([
["audio/ogg", "ogg"],
["audio/amr", "amr"],
["audio/x-wav", "wav"],
["audio/mpeg", "MPEG_layer_3"],
@ -75,7 +77,7 @@ Media.contentTypeToFormat = new Map([
["image/png", "PNG"],
]);
Media.supportedAudioFormats = ["MPEG_layer_3", "wav", "amr"];
Media.supportedAudioFormats = ["MPEG_layer_3", "wav", "amr", "ogg"];
Media.supportedImageFormats = ["JPEG", "PNG"];
Native.create("com/sun/mmedia/DefaultConfiguration.nListContentTypesOpen.(Ljava/lang/String;)I", function(jProtocol) {
@ -406,7 +408,25 @@ function PlayerContainer(url) {
// default buffer size 1 MB
PlayerContainer.DEFAULT_BUFFER_SIZE = 1024 * 1024;
PlayerContainer.prototype.isAudioCapture = function() {
return !!(this.url && this.url.startsWith("capture://audio"));
};
PlayerContainer.prototype.guessFormatFromURL = function() {
if (this.isAudioCapture()) {
var encoding = "audio/ogg"; // Same as system property |audio.encodings|
var idx = this.url.indexOf("encoding=");
if (idx > 0) {
var encodingKeyPair = this.url.substring(idx).split("&")[0].split("=");
encoding = encodingKeyPair.length == 2 ? encodingKeyPair[1] : encoding;
}
var format = Media.contentTypeToFormat.get(encoding);
return format || "UNKNOWN";
}
return Media.extToFormat.get(this.url.substr(this.url.lastIndexOf(".") + 1)) || "UNKNOWN";
}
@ -424,6 +444,9 @@ PlayerContainer.prototype.realize = function(contentType) {
if (Media.supportedAudioFormats.indexOf(this.mediaFormat) !== -1) {
this.player = new AudioPlayer(this);
if (this.isAudioCapture()) {
this.audioRecorder = new AudioRecorder();
}
this.player.realize().then(resolve);
} else if (Media.supportedImageFormats.indexOf(this.mediaFormat) !== -1) {
this.player = new ImagePlayer(this);
@ -476,6 +499,11 @@ PlayerContainer.prototype.getMediaFormat = function() {
return "mid";
}
// https://wiki.xiph.org/Ogg#Detecting_Ogg_files_and_extracting_information
if (headerString.indexOf("OggS") === 0) {
return "ogg";
}
return this.mediaFormat;
};
@ -553,6 +581,155 @@ PlayerContainer.prototype.setVisible = function(visible) {
this.player.setVisible(visible);
}
PlayerContainer.prototype.getRecordedSize = function() {
return this.audioRecorder.data.byteLength;
};
PlayerContainer.prototype.getRecordedData = function(offset, size, buffer) {
var toRead = (size < this.audioRecorder.data.length) ? size : this.audioRecorder.data.byteLength;
buffer.set(this.audioRecorder.data.subarray(0, toRead), offset);
this.audioRecorder.data = new Uint8Array(this.audioRecorder.data.buffer.slice(toRead));
};
var AudioRecorder = function(aMimeType) {
this.mimeType = aMimeType || "audio/ogg";
this.eventListeners = {};
this.data = new Uint8Array();
this.sender = DumbPipe.open("audiorecorder", {
mimeType: this.mimeType
}, this.recipient.bind(this));
};
AudioRecorder.prototype.recipient = function(message) {
var callback = this["on" + message.type];
if (typeof callback === "function") {
callback(message);
}
if (this.eventListeners[message.type]) {
this.eventListeners[message.type].forEach(function(listener) {
if (typeof listener === "function") {
listener(message);
}
});
}
};
AudioRecorder.prototype.addEventListener = function(name, callback) {
if (!callback || !name) {
return;
}
if (!this.eventListeners[name]) {
this.eventListeners[name] = [];
}
this.eventListeners[name].push(callback);
};
AudioRecorder.prototype.removeEventListener = function(name, callback) {
if (!name || !callback || !this.eventListeners[name]) {
return;
}
var newArray = [];
this.eventListeners[name].forEach(function(listener) {
if (callback != listener) {
newArray.push(listener);
}
});
this.eventListeners[name] = newArray;
};
AudioRecorder.prototype.start = function() {
return new Promise(function(resolve, reject) {
this.onstart = function() {
this.onstart = null;
this.onerror = null;
resolve(1);
}.bind(this);
this.onerror = function() {
this.onstart = null;
this.onerror = null;
resolve(0);
}.bind(this);
this.sender({ type: "start" });
}.bind(this));
};
AudioRecorder.prototype.stop = function() {
return new Promise(function(resolve, reject) {
// To make sure the Player in Java can fetch data immediately, we
// need to return after data is back.
this.ondata = function ondata(message) {
_cleanEventListeners();
// The audio data we received are encoded with a proper format, it doesn't
// make sense to concatenate them like the socket, so let just override
// the buffered data here.
this.data = new Uint8Array(message.data);
resolve(1);
}.bind(this);
var _onerror = function() {
_cleanEventListeners();
resolve(0);
}.bind(this);
var _cleanEventListeners = function() {
this.ondata = null;
this.removeEventListener("error", _onerror);
}.bind(this);
this.addEventListener("error", _onerror);
this.sender({ type: "stop" });
}.bind(this));
};
AudioRecorder.prototype.pause = function() {
return new Promise(function(resolve, reject) {
// In Java, |stopRecord| might be called before |commit|, which triggers
// the calling sequence:
// nPause -> nGetRecordedSize -> nGetRecordedData -> nClose
//
// to make sure the Player in Java can fetch data in such a case, we
// need to request data immediately.
//
this.ondata = function ondata(message) {
this.ondata = null;
// The audio data we received are encoded with a proper format, it doesn't
// make sense to concatenate them like the socket, so let just override
// the buffered data here.
this.data = new Uint8Array(message.data);
resolve(1);
}.bind(this);
// Have to request data first before pausing.
this.requestData();
this.sender({ type: "pause" });
}.bind(this));
};
AudioRecorder.prototype.requestData = function() {
this.sender({ type: "requestData" });
};
AudioRecorder.prototype.close = function() {
if (this._closed) {
return new Promise(function(resolve) { resolve(1); });
}
// Make sure recording is stopped on the other side.
return this.stop().then(function() {
DumbPipe.close(this.sender);
this._closed = true;
}.bind(this));
};
Native.create("com/sun/mmedia/PlayerImpl.nInit.(IILjava/lang/String;)I", function(appId, pId, jURI) {
var url = util.fromJavaString(jURI);
var id = pId + (appId << 32);
@ -593,7 +770,6 @@ Native.create("com/sun/mmedia/PlayerImpl.nRealize.(ILjava/lang/String;)Z", funct
return player.realize(mime);
}, true);
Native.create("com/sun/mmedia/MediaDownload.nGetJavaBufferSize.(I)I", function(handle) {
var player = Media.PlayerCache[handle];
return player.getBufferSize();
@ -746,6 +922,60 @@ Native.create("com/sun/mmedia/DirectPlayer.nSetVisible.(IZ)Z", function(handle,
return true;
});
Native.create("com/sun/mmedia/DirectPlayer.nIsRecordControlSupported.(I)Z", function(handle) {
return !!(Media.PlayerCache[handle] && Media.PlayerCache[handle].audioRecorder);
});
Native.create("com/sun/mmedia/DirectRecord.nSetLocator.(ILjava/lang/String;)I", function(handle, locator) {
console.warn("com/sun/mmedia/DirectRecord.nSetLocator.(I)I not implemented.");
return -1;
});
Native.create("com/sun/mmedia/DirectRecord.nGetRecordedSize.(I)I", function(handle) {
return Media.PlayerCache[handle].getRecordedSize();
});
Native.create("com/sun/mmedia/DirectRecord.nGetRecordedData.(III[B)I", function(handle, offset, size, buffer) {
Media.PlayerCache[handle].getRecordedData(offset, size, buffer);
return 1;
});
Native.create("com/sun/mmedia/DirectRecord.nCommit.(I)I", function(handle) {
// In DirectRecord.java, before nCommit, nPause or nStop is called,
// which means all the recorded data has been fetched, so do nothing here.
return 1;
});
Native.create("com/sun/mmedia/DirectRecord.nPause.(I)I", function(handle) {
return Media.PlayerCache[handle].audioRecorder.pause();
}, true);
Native.create("com/sun/mmedia/DirectRecord.nStop.(I)I", function(handle) {
return Media.PlayerCache[handle].audioRecorder.stop();
}, true);
Native.create("com/sun/mmedia/DirectRecord.nClose.(I)I", function(handle) {
var player = Media.PlayerCache[handle];
if (!player || !player.audioRecorder) {
// We need to check if |audioRecorder| is still available, because |nClose|
// might be called twice in DirectRecord.java, and only IOException is
// handled in DirectRecord.java, let use IOException instead of IllegalStateException.
throw new JavaException("java/io/IOException");
}
return player.audioRecorder.close().then(function(result) {
delete player.audioRecorder;
return result;
});
}, true);
Native.create("com/sun/mmedia/DirectRecord.nStart.(I)I", function(handle) {
// In DirectRecord.java, nStart plays two roles: real start and resume.
// Let's handle this on the other side of the DumbPipe.
return Media.PlayerCache[handle].audioRecorder.start();
}, true);
/**
* @return the volume level between 0 and 100 if succeeded. Otherwise -1.
*/

Просмотреть файл

@ -80,9 +80,9 @@ Native.create("com/sun/midp/io/j2me/socket/Protocol.open0.([BI)V", function(ipBy
this.socket.ondata = (function(message) {
// console.log("this.socket.ondata: " + JSON.stringify(message));
var newArray = new Uint8Array(this.data.byteLength + message.data.length);
var newArray = new Uint8Array(this.data.byteLength + message.data.byteLength);
newArray.set(this.data);
newArray.set(message.data, this.data.byteLength);
newArray.set(new Uint8Array(message.data), this.data.byteLength);
this.data = newArray;
if (this.waitingData) {

Просмотреть файл

@ -76,6 +76,9 @@ Native.create("java/lang/System.getProperty0.(Ljava/lang/String;)Ljava/lang/Stri
case "microedition.amms.version":
value = "1.1";
break;
case "microedition.media.version":
value = '1.2';
break;
case "mmapi-configuration":
value = null;
break;
@ -143,6 +146,10 @@ Native.create("java/lang/System.getProperty0.(Ljava/lang/String;)Ljava/lang/Stri
console.warn("Property 'com.nokia.multisim.imsi.sim2' is a stub");
value = null;
break;
case "com.nokia.mid.batterylevel":
// http://developer.nokia.com/community/wiki/Checking_battery_level_in_Java_ME
value = Math.floor(navigator.battery.level * 100).toString();
break;
case "com.nokia.mid.imsi":
console.warn("Property 'com.nokia.mid.imsi' is a stub");
value = "000000000000000";
@ -170,6 +177,15 @@ Native.create("java/lang/System.getProperty0.(Ljava/lang/String;)Ljava/lang/Stri
case "classpathext":
value = null;
break;
case "supports.audio.capture":
value = "true";
break;
case "supports.recording":
value = "true";
break;
case "audio.encodings":
value = "audio/ogg";
break;
default:
console.warn("UNKNOWN PROPERTY (java/lang/System): " + util.fromJavaString(key));
value = null;

Просмотреть файл

@ -0,0 +1,166 @@
package org.mozilla;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.ItemCommandListener;
import javax.microedition.lcdui.StringItem;
import javax.microedition.media.Manager;
import javax.microedition.media.MediaException;
import javax.microedition.media.Player;
import javax.microedition.media.control.RecordControl;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
import javax.microedition.media.PlayerListener;
public class AudioRecorder extends MIDlet implements CommandListener,
ItemCommandListener {
private Form form;
private Display display;
private StringItem recordButton;
private boolean recording = false;
private Player player = null;
private RecordControl recordControl = null;
private ByteArrayOutputStream outputStream = null;
private void logMessage(String msg) {
this.form.insert(2, new StringItem(null, msg));
}
private class StopThread extends Thread {
public void run() {
try {
recordControl.stopRecord();
recordControl.commit();
player.stop();
player.close();
logMessage("Start to playback the recorded audio.");
String audioEncodings = System.getProperty("audio.encodings");
Player playback = Manager.createPlayer(
new ByteArrayInputStream(outputStream.toByteArray()),
(audioEncodings != null && audioEncodings.trim() != "") ? audioEncodings : "audio/amr");
playback.realize();
playback.prefetch();
playback.start();
} catch (IOException e) {
logMessage("Error occurs when stop recording: " + e);
} catch (MediaException e) {
logMessage("Error occurs when stop recording: " + e);
}
logMessage("Recording stopped.");
}
}
private class RecordingThread extends Thread {
public void run() {
logMessage("Start a new thread to record audio.");
try {
player = Manager.createPlayer("capture://audio");
player.realize();
player.addPlayerListener(new PlayerListener() {
public void playerUpdate(Player player, String event, Object eventData) {
if (PlayerListener.RECORD_ERROR.equals(event)) {
logMessage("Error occurs when start recording: " + eventData);
recording = false;
updateRecordingMessage();
}
}
});
recordControl = (RecordControl) player
.getControl("RecordControl");
outputStream = new ByteArrayOutputStream();
recordControl.setRecordStream(outputStream);
recordControl.startRecord();
player.start();
} catch (IOException e) {
recording = false;
updateRecordingMessage();
logMessage("Error occurs when capturing audio: " + e);
} catch (MediaException e) {
recording = false;
updateRecordingMessage();
logMessage("Error occurs when capturing audio: " + e);
}
}
}
public AudioRecorder() {
}
private void stopRecording() {
this.recording = false;
this.updateRecordingMessage();
new StopThread().start();
}
private void startRecording() {
this.recording = true;
this.updateRecordingMessage();
new RecordingThread().start();
}
private void updateRecordingMessage() {
this.recordButton.setText(this.recording ? "Stop" : "Start");
}
private void toggleRecorderStatus() {
// Check if the device support audio capturing.
if (!"true".equals(System.getProperty("supports.audio.capture"))) {
this.logMessage("This device doesn't support audio capture!");
return;
}
if (this.recording) {
this.stopRecording();
} else {
this.startRecording();
}
}
public void commandAction(Command command, Item item) {
if (item == this.recordButton) {
this.toggleRecorderStatus();
}
}
public void commandAction(Command c, Displayable d) {
}
protected void destroyApp(boolean unconditional)
throws MIDletStateChangeException {
}
protected void pauseApp() {
}
protected void startApp() throws MIDletStateChangeException {
this.recordButton = new StringItem(null, "Start", Item.BUTTON);
Command toggleRecordingCMD = new Command("Click", Command.ITEM, 1);
this.recordButton.addCommand(toggleRecordingCMD);
this.recordButton.setDefaultCommand(toggleRecordingCMD);
this.recordButton.setItemCommandListener(this);
this.form = new Form(null, new Item[] {
new StringItem(null, "Audio Recorder"), this.recordButton });
this.display = Display.getDisplay(this);
this.display.setCurrent(this.form);
}
}