W3C Media Capture API

An implementation of the W3C Media Capture spec:
http://dev.w3.org/2009/dap/camera/Overview-API

Capture operations are supported for audio, video, and images.  Each
capture operation launches the native audio recorder, video recorder,
or camera application, respectively.
This commit is contained in:
macdonst 2011-03-11 16:14:47 -05:00
parent d74569ffa7
commit e766188689
6 changed files with 539 additions and 8 deletions

View File

@ -0,0 +1,187 @@
/*
* PhoneGap is available under *either* the terms of the modified BSD license *or* the
* MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
*
* Copyright (c) 2005-2010, Nitobi Software Inc.
* Copyright (c) 2010-2011, IBM Corporation
*/
/**
* The CaptureError interface encapsulates all errors in the Capture API.
*/
function CaptureError() {
this.code = null;
};
// Capture error codes
CaptureError.CAPTURE_INTERNAL_ERR = 0;
CaptureError.CAPTURE_APPLICATION_BUSY = 1;
CaptureError.CAPTURE_INVALID_ARGUMENT = 2;
CaptureError.CAPTURE_NO_MEDIA_FILES = 3;
CaptureError.CAPTURE_NOT_SUPPORTED = 20;
/**
* The Capture interface exposes an interface to the camera and microphone of the hosting device.
*/
function Capture() {
this.supportedAudioFormats = [];
this.supportedImageFormats = [];
this.supportedVideoFormats = [];
};
/**
* Launch audio recorder application for recording audio clip(s).
*
* @param {Function} successCB
* @param {Function} errorCB
* @param {CaptureAudioOptions} options
*/
Capture.prototype.captureAudio = function(successCallback, errorCallback, options) {
PhoneGap.exec(successCallback, errorCallback, "Capture", "captureAudio", [options]);
};
/**
* Launch camera application for taking image(s).
*
* @param {Function} successCB
* @param {Function} errorCB
* @param {CaptureImageOptions} options
*/
Capture.prototype.captureImage = function(successCallback, errorCallback, options) {
PhoneGap.exec(successCallback, errorCallback, "Capture", "captureImage", [options]);
};
/**
* Launch camera application for taking image(s).
*
* @param {Function} successCB
* @param {Function} errorCB
* @param {CaptureImageOptions} options
*/
Capture.prototype._castMediaFile = function(pluginResult) {
var mediaFiles = [];
var i;
for (i=0; i<pluginResult.message.length; i++) {
var mediaFile = new MediaFile();
mediaFile.name = pluginResult.message[i].name;
mediaFile.fullPath = pluginResult.message[i].fullPath;
mediaFile.type = pluginResult.message[i].type;
mediaFile.lastModifiedDate = pluginResult.message[i].lastModifiedDate;
mediaFile.size = pluginResult.message[i].size;
mediaFiles.push(mediaFile);
}
pluginResult.message = mediaFiles;
return pluginResult;
};
/**
* Launch device camera application for recording video(s).
*
* @param {Function} successCB
* @param {Function} errorCB
* @param {CaptureVideoOptions} options
*/
Capture.prototype.captureVideo = function(successCallback, errorCallback, options) {
PhoneGap.exec(successCallback, errorCallback, "Capture", "captureVideo", [options]);
};
/**
* Encapsulates a set of parameters that the capture device supports.
*/
function ConfigurationData() {
// The ASCII-encoded string in lower case representing the media type.
this.type;
// The height attribute represents height of the image or video in pixels.
// In the case of a sound clip this attribute has value 0.
this.height = 0;
// The width attribute represents width of the image or video in pixels.
// In the case of a sound clip this attribute has value 0
this.width = 0;
};
/**
* Encapsulates all image capture operation configuration options.
*/
function CaptureImageOptions() {
// Upper limit of images user can take. Value must be equal or greater than 1.
this.limit = 1;
// The selected image mode. Must match with one of the elements in supportedImageModes array.
this.mode;
};
/**
* Encapsulates all video capture operation configuration options.
*/
function CaptureVideoOptions() {
// Upper limit of videos user can record. Value must be equal or greater than 1.
this.limit;
// Maximum duration of a single video clip in seconds.
this.duration;
// The selected video mode. Must match with one of the elements in supportedVideoModes array.
this.mode;
};
/**
* Encapsulates all audio capture operation configuration options.
*/
function CaptureAudioOptions() {
// Upper limit of sound clips user can record. Value must be equal or greater than 1.
this.limit;
// Maximum duration of a single sound clip in seconds.
this.duration;
// The selected audio mode. Must match with one of the elements in supportedAudioModes array.
this.mode;
};
/**
* Represents a single file.
*
* name {DOMString} name of the file, without path information
* fullPath {DOMString} the full path of the file, including the name
* type {DOMString} mime type
* lastModifiedDate {Date} last modified date
* size {Number} size of the file in bytes
*/
function MediaFile(name, fullPath, type, lastModifiedDate, size) {
this.name = name || null;
this.fullPath = fullPath || null;
this.type = type || null;
this.lastModifiedDate = lastModifiedDate || null;
this.size = size || 0;
}
/**
* Launch device camera application for recording video(s).
*
* @param {Function} successCB
* @param {Function} errorCB
*/
MediaFile.prototype.getFormatData = function(successCallback, errorCallback) {
PhoneGap.exec(successCallback, errorCallback, "Capture", "getFormatData", [this.fullPath, this.type]);
};
/**
* MediaFileData encapsulates format information of a media file.
*
* @param {DOMString} codecs
* @param {long} bitrate
* @param {long} height
* @param {long} width
* @param {float} duration
*/
function MediaFileData(codecs, bitrate, height, width, duration) {
this.codecs = codecs || null;
this.bitrate = bitrate || 0;
this.height = height || 0;
this.width = width || 0;
this.duration = duration || 0;
}
PhoneGap.addConstructor(function() {
if (typeof navigator.device === "undefined") {
navigator.device = window.device = new Device();
}
if (typeof navigator.device.capture === "undefined") {
navigator.device.capture = window.device.capture = new Capture();
}
});

View File

@ -98,6 +98,8 @@ Device.prototype.exitApp = function() {
};
PhoneGap.addConstructor(function() {
navigator.device = window.device = new Device();
if (typeof navigator.device === "undefined") {
navigator.device = window.device = new Device();
}
});
};

View File

@ -9,11 +9,6 @@
if (!PhoneGap.hasResource("file")) {
PhoneGap.addResource("file");
/**
* This class provides generic read and write access to the mobile device file system.
* They are not used to read files from a server.
*/
/**
* This class provides some useful information about a file.
* This is the fields returned when navigator.fileMgr.getFileProperties()

View File

@ -0,0 +1,346 @@
package com.phonegap;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
public class Capture extends Plugin {
private static final String _DATA = "_data"; // The column name where the file path is stored
private static final int CAPTURE_AUDIO = 0; // Constant for capture audio
private static final int CAPTURE_IMAGE = 1; // Constant for capture image
private static final int CAPTURE_VIDEO = 2; // Constant for capture video
private static final String LOG_TAG = "Capture";
private String callbackId; // The ID of the callback to be invoked with our result
private long limit; // the number of pics/vids/clips to take
private double duration; // optional duration parameter for video recording
private JSONArray results; // The array of results to be returned to the user
private Uri imageUri; // Uri of captured image
@Override
public PluginResult execute(String action, JSONArray args, String callbackId) {
this.callbackId = callbackId;
this.limit = 1;
this.duration = 0.0f;
this.results = new JSONArray();
JSONObject options = args.optJSONObject(0);
if (options != null) {
limit = options.optLong("limit", 1);
duration = options.optDouble("duration", 0.0f);
}
if (action.equals("getFormatData")) {
try {
JSONObject obj = getFormatData(args.getString(0), args.getString(1));
return new PluginResult(PluginResult.Status.OK, obj);
} catch (JSONException e) {
return new PluginResult(PluginResult.Status.ERROR);
}
}
else if (action.equals("captureAudio")) {
this.captureAudio();
}
else if (action.equals("captureImage")) {
this.captureImage();
}
else if (action.equals("captureVideo")) {
this.captureVideo(duration);
}
PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
r.setKeepCallback(true);
return r;
}
/**
* Provides the media data file data depending on it's mime type
*
* @param filePath path to the file
* @param mimeType of the file
* @return a MediaFileData object
*/
private JSONObject getFormatData(String filePath, String mimeType) {
JSONObject obj = new JSONObject();
try {
// setup defaults
obj.put("height", 0);
obj.put("width", 0);
obj.put("bitrate", 0);
obj.put("duration", 0);
obj.put("codecs", "");
if (mimeType.equals("image/jpeg")) {
obj = getImageData(filePath, obj);
}
else if (filePath.endsWith("audio/3gpp")) {
obj = getAudioVideoData(filePath, obj, false);
}
else if (mimeType.equals("video/3gpp")) {
obj = getAudioVideoData(filePath, obj, true);
}
}
catch (JSONException e) {
Log.d(LOG_TAG, "Error: setting media file data object");
}
return obj;
}
/**
* Get the Image specific attributes
*
* @param filePath path to the file
* @param obj represents the Media File Data
* @return a JSONObject that represents the Media File Data
* @throws JSONException
*/
private JSONObject getImageData(String filePath, JSONObject obj) throws JSONException {
Bitmap bitmap = BitmapFactory.decodeFile(filePath);
obj.put("height", bitmap.getHeight());
obj.put("width", bitmap.getWidth());
return obj;
}
/**
* Get the Image specific attributes
*
* @param filePath path to the file
* @param obj represents the Media File Data
* @param video if true get video attributes as well
* @return a JSONObject that represents the Media File Data
* @throws JSONException
*/
private JSONObject getAudioVideoData(String filePath, JSONObject obj, boolean video) throws JSONException {
MediaPlayer player = new MediaPlayer();
try {
player.setDataSource(filePath);
player.prepare();
obj.put("duration", player.getDuration());
if (video) {
obj.put("height", player.getVideoHeight());
obj.put("width", player.getVideoWidth());
}
}
catch (IOException e) {
Log.d(LOG_TAG, "Error: loading video file");
}
return obj;
}
/**
* Sets up an intent to capture audio. Result handled by onActivityResult()
*/
private void captureAudio() {
Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION);
this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_AUDIO);
}
/**
* Sets up an intent to capture images. Result handled by onActivityResult()
*/
private void captureImage() {
Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
// Specify file so that large image is captured and returned
File photo = new File(Environment.getExternalStorageDirectory(), "Capture.jpg");
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo));
this.imageUri = Uri.fromFile(photo);
this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_IMAGE);
}
/**
* Sets up an intent to capture video. Result handled by onActivityResult()
*/
private void captureVideo(double duration) {
Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE);
// Introduced in API 8
//intent.putExtra(android.provider.MediaStore.EXTRA_DURATION_LIMIT, duration);
this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_VIDEO);
}
/**
* Called when the video view exits.
*
* @param requestCode The request code originally supplied to startActivityForResult(),
* allowing you to identify who this result came from.
* @param resultCode The integer result code returned by the child activity through its setResult().
* @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
* @throws JSONException
*/
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
// Result received okay
if (resultCode == Activity.RESULT_OK) {
// An audio clip was requested
if (requestCode == CAPTURE_AUDIO) {
// Get the uri of the audio clip
Uri data = intent.getData();
// create a file object from the uri
results.put(createMediaFile(data));
if (results.length() >= limit) {
// Send Uri back to JavaScript for listening to audio
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
} else {
// still need to capture more audio clips
captureAudio();
}
} else if (requestCode == CAPTURE_IMAGE) {
// For some reason if I try to do:
// Uri data = intent.getData();
// It crashes in the emulator and on my phone with a null pointer exception
// To work around it I had to grab the code from CameraLauncher.java
try {
// Read in bitmap of captured image
Bitmap bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri);
// Create entry in media store for image
// (Don't use insertImage() because it uses default compression setting of 50 - no way to change it)
ContentValues values = new ContentValues();
values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
Uri uri = null;
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException e) {
System.out.println("Can't write to external media storage.");
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException ex) {
System.out.println("Can't write to internal media storage.");
this.fail("Error capturing image - no media storage found.");
return;
}
}
// Add compressed version of captured image to returned media store Uri
OutputStream os = this.ctx.getContentResolver().openOutputStream(uri);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
os.close();
bitmap.recycle();
bitmap = null;
System.gc();
// Add image to results
results.put(createMediaFile(uri));
if (results.length() >= limit) {
// Send Uri back to JavaScript for viewing image
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
} else {
// still need to capture more images
captureImage();
}
} catch (IOException e) {
e.printStackTrace();
this.fail("Error capturing image.");
}
} else if (requestCode == CAPTURE_VIDEO) {
// Get the uri of the video clip
Uri data = intent.getData();
// create a file object from the uri
results.put(createMediaFile(data));
if (results.length() >= limit) {
// Send Uri back to JavaScript for viewing video
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
} else {
// still need to capture more video clips
captureVideo(duration);
}
}
}
// If canceled
else if (resultCode == Activity.RESULT_CANCELED) {
// If we have partial results send them back to the user
if (results.length() > 0) {
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
}
// user canceled the action
else {
this.fail("Canceled.");
}
}
// If something else
else {
// If we have partial results send them back to the user
if (results.length() > 0) {
this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId);
}
// something bad happened
else {
this.fail("Did not complete!");
}
}
}
/**
* Creates a JSONObject that represents a File from the Uri
*
* @param data the Uri of the audio/image/video
* @return a JSONObject that represents a File
*/
private JSONObject createMediaFile(Uri data) {
File fp = new File(getRealPathFromURI(data));
JSONObject obj = new JSONObject();
try {
// File properties
obj.put("name", fp.getName());
obj.put("fullPath", fp.getAbsolutePath());
obj.put("type", FileUtils.getMimeType(fp.getAbsolutePath()));
obj.put("lastModifiedDate", fp.lastModified());
obj.put("size", fp.length());
} catch (JSONException e) {
// this will never happen
e.printStackTrace();
}
return obj;
}
/**
* Queries the media store to find out what the file path is for the Uri we supply
*
* @param contentUri the Uri of the audio/image/video
* @return the full path to the file
*/
private String getRealPathFromURI(Uri contentUri) {
String[] proj = { _DATA };
Cursor cursor = this.ctx.managedQuery(contentUri, proj, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(_DATA);
cursor.moveToFirst();
return cursor.getString(column_index);
}
/**
* Send error message to JavaScript.
*
* @param err
*/
public void fail(String err) {
this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId);
}
}

View File

@ -287,6 +287,7 @@ public class DroidGap extends PhonegapActivity {
this.addService("Storage", "com.phonegap.Storage");
this.addService("Temperature", "com.phonegap.TempListener");
this.addService("FileTransfer", "com.phonegap.FileTransfer");
this.addService("Capture", "com.phonegap.Capture");
}
/**

View File

@ -3,7 +3,7 @@
* MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
*
* Copyright (c) 2005-2010, Nitobi Software Inc.
* Copyright (c) 2010, IBM Corporation
* Copyright (c) 2010-2011, IBM Corporation
*/
package com.phonegap;
@ -942,7 +942,7 @@ public class FileUtils extends Plugin {
* @param filename
* @return a mime type
*/
private String getMimeType(String filename) {
public static String getMimeType(String filename) {
MimeTypeMap map = MimeTypeMap.getSingleton();
return map.getMimeTypeFromExtension(map.getFileExtensionFromUrl(filename));
}