/* * 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) 2011, IBM Corporation */ package com.phonegap.media; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; import javax.microedition.media.Manager; import com.phonegap.api.Plugin; import com.phonegap.api.PluginResult; import com.phonegap.file.File; import com.phonegap.json4j.JSONArray; import com.phonegap.util.Logger; import com.phonegap.util.StringUtils; /** * This plugin provides the ability to capture media from the native media * applications. The appropriate media application is launched, and a capture * operation is started in the background to identify captured media files and * return the file info back to the caller. */ public class MediaCapture extends Plugin { public static String PROTOCOL_CAPTURE = "capture"; /** * Error codes. */ // Camera or microphone failed to capture image or sound. private static final int CAPTURE_INTERNAL_ERR = 0; // Camera application or audio capture application is currently serving other capture request. private static final int CAPTURE_APPLICATION_BUSY = 1; // Invalid use of the API (e.g. limit parameter has value less than one). private static final int CAPTURE_INVALID_ARGUMENT = 2; // User exited camera application or audio capture application before capturing anything. private static final int CAPTURE_NO_MEDIA_FILES = 3; // The requested capture operation is not supported. private static final int CAPTURE_NOT_SUPPORTED = 20; /** * Possible actions. */ protected static final int ACTION_GET_SUPPORTED_AUDIO_MODES = 0; protected static final int ACTION_GET_SUPPORTED_IMAGE_MODES = 1; protected static final int ACTION_GET_SUPPORTED_VIDEO_MODES = 2; protected static final int ACTION_CAPTURE_AUDIO = 3; protected static final int ACTION_CAPTURE_IMAGE = 4; protected static final int ACTION_CAPTURE_VIDEO = 5; protected static final int ACTION_CANCEL_CAPTURES = 6; /** * Executes the requested action and returns a PluginResult. * * @param action * The action to execute. * @param callbackId * The callback ID to be invoked upon action completion * @param args * JSONArry of arguments for the action. * @return A PluginResult object with a status and message. */ public PluginResult execute(String action, JSONArray args, String callbackId) { switch (getAction(action)) { case ACTION_GET_SUPPORTED_AUDIO_MODES: return getAudioCaptureModes(); case ACTION_GET_SUPPORTED_IMAGE_MODES: return getImageCaptureModes(); case ACTION_GET_SUPPORTED_VIDEO_MODES: return getVideoCaptureModes(); case ACTION_CAPTURE_AUDIO: return captureAudio(args, callbackId); case ACTION_CAPTURE_IMAGE: return captureImage(args, callbackId); case ACTION_CAPTURE_VIDEO: return captureVideo(args, callbackId); case ACTION_CANCEL_CAPTURES: CaptureControl.getCaptureControl().stopPendingOperations(true); return new PluginResult(PluginResult.Status.OK); } return new PluginResult(PluginResult.Status.INVALID_ACTION, "MediaCapture: invalid action " + action); } /** * Determines if audio capture is supported. * @return <code>true</code> if audio capture is supported */ protected boolean isAudioCaptureSupported() { return (System.getProperty("supports.audio.capture").equals(Boolean.TRUE.toString()) && AudioControl.hasAudioRecorderApplication()); } /** * Determines if video capture is supported. * @return <code>true</code> if video capture is supported */ protected boolean isVideoCaptureSupported() { return (System.getProperty("supports.video.capture").equals(Boolean.TRUE.toString())); } /** * Retrieves supported audio capture modes (content types). * @return supported audio capture modes */ protected PluginResult getAudioCaptureModes() { if (!isAudioCaptureSupported()) { // if audio capture is not supported, return an empty array // of capture modes Logger.log(this.getClass().getName() + ": audio capture not supported"); return new PluginResult(PluginResult.Status.OK, "[]"); } // get all supported capture content types String[] contentTypes = getCaptureContentTypes(); // return audio content types only JSONArray modes = new JSONArray(); for (int i = 0; i < contentTypes.length; i++) { if (contentTypes[i].startsWith(AudioCaptureOperation.CONTENT_TYPE)) { modes.add(new CaptureMode(contentTypes[i]).toJSONObject()); } } return new PluginResult(PluginResult.Status.OK, modes.toString()); } /** * Retrieves supported image capture modes (content type, width and height). * @return supported image capture modes */ protected PluginResult getImageCaptureModes() { // get supported capture content types String[] contentTypes = getCaptureContentTypes(); // need to get the recording dimensions from supported image encodings String imageEncodings = System.getProperty("video.snapshot.encodings"); Logger.log(this.getClass().getName() + ": video.snapshot.encodings=" + imageEncodings); String[] encodings = StringUtils.split(imageEncodings, "encoding="); // find matching encodings and parse them for dimensions // it's so annoying that we have to do this CaptureMode mode = null; Vector list = new Vector(); JSONArray modes = new JSONArray(); for (int i = 0; i < contentTypes.length; i++) { if (contentTypes[i].startsWith(ImageCaptureOperation.CONTENT_TYPE)) { String type = contentTypes[i].substring(ImageCaptureOperation.CONTENT_TYPE.length()); for (int j = 0; j < encodings.length; j++) { // format: "jpeg&width=2592&height=1944 " String enc = encodings[j]; if (enc.startsWith(type)) { Hashtable parms = parseEncodingString(enc); // "width=" String w = (String)parms.get("width"); long width = (w == null) ? 0 : Long.parseLong(w); // "height=" String h = (String)parms.get("height"); long height = (h == null) ? 0 : Long.parseLong(h); // new capture mode mode = new CaptureMode(contentTypes[i], width, height); // don't want duplicates if (!list.contains(mode)) { list.addElement(mode); modes.add(mode.toJSONObject()); } } } } } return new PluginResult(PluginResult.Status.OK, modes.toString()); } /** * Retrieves supported video capture modes (content type, width and height). * @return supported video capture modes */ protected PluginResult getVideoCaptureModes() { if (!isVideoCaptureSupported()) { // if the device does not support video capture, return an empty // array of capture modes Logger.log(this.getClass().getName() + ": video capture not supported"); return new PluginResult(PluginResult.Status.OK, "[]"); } /** * DOH! Even if video capture is supported, BlackBerry's API * does not provide any 'video/' content types for the 'capture' * protocol. So if we looked at only capture content types, * it wouldn't return any results... * * // get all supported capture content types * String[] contentTypes = getCaptureContentTypes(); * * A better alternative, and probably not too inaccurate, would be to * send back all supported video modes (not just capture). This will * at least give the developer an idea of the capabilities. */ // retrieve ALL supported video encodings String videoEncodings = System.getProperty("video.encodings"); Logger.log(this.getClass().getName() + ": video.encodings=" + videoEncodings); String[] encodings = StringUtils.split(videoEncodings, "encoding="); // parse them into CaptureModes JSONArray modes = new JSONArray(); String enc = null; CaptureMode mode = null; Vector list = new Vector(); for (int i = 0; i < encodings.length; i++) { enc = encodings[i]; // format: "video/3gpp&width=640&height=480&video_codec=MPEG-4&audio_codec=AAC " if (enc.startsWith(VideoCaptureOperation.CONTENT_TYPE)) { Hashtable parms = parseEncodingString(enc); // type "video/3gpp" String t = (String)parms.get("type"); // "width=" String w = (String)parms.get("width"); long width = (w == null) ? 0 : Long.parseLong(w); // "height=" String h = (String)parms.get("height"); long height = (h == null) ? 0 : Long.parseLong(h); // new capture mode mode = new CaptureMode(t, width, height); // don't want duplicates if (!list.contains(mode)) { list.addElement(mode); modes.add(mode.toJSONObject()); } } } return new PluginResult(PluginResult.Status.OK, modes.toString()); } /** * Utility method to parse encoding strings. * * @param encodingString * encoding string * @return Hashtable containing key:value pairs */ protected Hashtable parseEncodingString(final String encodingString) { // format: "video/3gpp&width=640&height=480&video_codec=MPEG-4&audio_codec=AAC " Hashtable props = new Hashtable(); String[] parms = StringUtils.split(encodingString, "&"); props.put("type", parms[0]); for (int i = 0; i < parms.length; i++) { String parameter = parms[i]; if (parameter.indexOf('=') != -1) { String[] pair = StringUtils.split(parameter, "="); props.put(pair[0].trim(), pair[1].trim()); } } return props; } /** * Returns the content types supported for the <code>capture://</code> * protocol. * * @return list of supported capture content types */ protected static String[] getCaptureContentTypes() { // retrieve list of all content types supported for capture protocol return Manager.getSupportedContentTypes(PROTOCOL_CAPTURE); } /** * Starts an audio capture operation using the native voice notes recorder * application. If the native voice notes recorder application is already * running, the <code>CAPTURE_APPLICATION_BUSY</code> error is returned. * * @param args * capture options (e.g., limit) * @param callbackId * the callback to be invoked with the capture results * @return PluginResult containing captured media file properties */ protected PluginResult captureAudio(final JSONArray args, final String callbackId) { PluginResult result = null; // if audio is not being recorded, start audio capture if (!AudioControl.hasAudioRecorderApplication()) { Logger.log(this.getClass().getName() + ": Audio recorder application is not installed."); result = new PluginResult(PluginResult.Status.ERROR, CAPTURE_NOT_SUPPORTED); } else if (AudioControl.isAudioRecorderActive()) { Logger.log(this.getClass().getName() + ": Audio recorder application is busy."); result = new PluginResult(PluginResult.Status.ERROR, CAPTURE_APPLICATION_BUSY); } else { // optional parameters int limit = args.optInt(0, 1); long duration = args.optLong(1, 0); // start audio capture // start capture operation in the background CaptureControl.getCaptureControl().startAudioCaptureOperation( limit, duration, callbackId); // return NO_RESULT and allow callbacks to be invoked later result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); } return result; } /** * Starts an image capture operation using the native camera application. If * the native camera application is already running, the * <code>CAPTURE_APPLICATION_BUSY</code> error is returned. * * @param args * capture options (e.g., limit) * @param callbackId * the callback to be invoked with the capture results * @return PluginResult containing captured media file properties */ protected PluginResult captureImage(final JSONArray args, final String callbackId) { PluginResult result = null; if (CameraControl.isCameraActive()) { Logger.log(this.getClass().getName() + ": Camera application is busy."); result = new PluginResult(PluginResult.Status.ERROR, CAPTURE_APPLICATION_BUSY); } else { // optional parameters int limit = args.optInt(0, 1); // start capture operation in the background CaptureControl.getCaptureControl().startImageCaptureOperation( limit, callbackId); // return NO_RESULT and allow callbacks to be invoked later result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); } return result; } /** * Starts an video capture operation using the native video recorder * application. If the native video recorder application is already running, * the <code>CAPTURE_APPLICATION_BUSY</code> error is returned. * * @param args * capture options (e.g., limit) * @param callbackId * the callback to be invoked with the capture results * @return PluginResult containing captured media file properties */ protected PluginResult captureVideo(final JSONArray args, final String callbackId) { PluginResult result = null; if (!isVideoCaptureSupported()) { Logger.log(this.getClass().getName() + ": Video capture is not supported."); result = new PluginResult(PluginResult.Status.ERROR, CAPTURE_NOT_SUPPORTED); } else if (CameraControl.isVideoRecorderActive()) { Logger.log(this.getClass().getName() + ": Video recorder application is busy."); result = new PluginResult(PluginResult.Status.ERROR, CAPTURE_APPLICATION_BUSY); } else { // optional parameters int limit = args.optInt(0, 1); // start capture operation in the background CaptureControl.getCaptureControl().startVideoCaptureOperation( limit, callbackId); // return NO_RESULT and allow callbacks to be invoked later result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); } return result; } /** * Sends media capture result back to JavaScript. * * @param mediaFiles * list of File objects describing captured media files * @param callbackId * the callback to receive the file descriptions */ public static void captureSuccess(Vector mediaFiles, String callbackId) { PluginResult result = null; File file = null; JSONArray array = new JSONArray(); for (Enumeration e = mediaFiles.elements(); e.hasMoreElements();) { file = (File) e.nextElement(); array.add(file.toJSONObject()); } // invoke the appropriate callback result = new PluginResult(PluginResult.Status.OK, array.toString()); success(result, callbackId); } /** * Sends error back to JavaScript. * * @param callbackId * the callback to receive the error */ public static void captureError(String callbackId) { error(new PluginResult(PluginResult.Status.ERROR, CAPTURE_NO_MEDIA_FILES), callbackId); } /** * Called when application is resumed. */ public void onResume() { // We launch the native media applications for capture operations, which // puts this application in the background. This application will come // to the foreground when the user closes the native media application. // So we close any running capture operations any time we resume. // // It would be nice if we could catch the EVT_APP_FOREGROUND event that // is supposed to be triggered when the application comes to the // foreground, but have not seen a way to do that on the Java side. // Unfortunately, we have to get notification from the JavaScript side, // which does get the event. (Argh! Only BlackBerry.) // // In this case, we're just stopping the capture operations, not // canceling them. CaptureControl.getCaptureControl().stopPendingOperations(false); } /** * Invoked when this application terminates. */ public void onDestroy() { CaptureControl.getCaptureControl().stopPendingOperations(true); } /** * Returns action to perform. * @param action * @return action to perform */ protected static int getAction(String action) { if ("getSupportedAudioModes".equals(action)) { return ACTION_GET_SUPPORTED_AUDIO_MODES; } if ("getSupportedImageModes".equals(action)) { return ACTION_GET_SUPPORTED_IMAGE_MODES; } if ("getSupportedVideoModes".equals(action)) { return ACTION_GET_SUPPORTED_VIDEO_MODES; } if ("captureAudio".equals(action)) { return ACTION_CAPTURE_AUDIO; } if ("captureImage".equals(action)) { return ACTION_CAPTURE_IMAGE; } if ("captureVideo".equals(action)) { return ACTION_CAPTURE_VIDEO; } if ("stopCaptures".equals(action)) { return ACTION_CANCEL_CAPTURES; } return -1; } }