-
-
Save epologee/4d859f0c030342a682d4 to your computer and use it in GitHub Desktop.
Put an end to all AS3 webcam access misery...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.epologee.puremvc.model.camera { | |
import com.epologee.development.logging.critical; | |
import com.epologee.development.logging.debug; | |
import com.epologee.development.logging.error; | |
import com.epologee.development.logging.info; | |
import com.epologee.development.logging.warn; | |
import com.epologee.time.Stopwatch; | |
import com.epologee.time.TimeDelay; | |
import org.puremvc.as3.multicore.patterns.proxy.Proxy; | |
import flash.display.Stage; | |
import flash.events.ActivityEvent; | |
import flash.events.Event; | |
import flash.events.StatusEvent; | |
import flash.events.TimerEvent; | |
import flash.media.Camera; | |
import flash.media.Video; | |
import flash.utils.getQualifiedClassName; | |
/** | |
* @author Eric-Paul Lecluse (c) epologee.com | |
*/ | |
public class WebCamProxy extends Proxy { | |
public static const NAME : String = getQualifiedClassName(WebCamProxy2); | |
// | |
public static const SHOW_CAMERA_ACCESS_DIALOG : String = NAME + ":SHOW_CAMERA_ACCESS_DIALOG"; | |
public static const HIDE_CAMERA_ACCESS_DIALOG : String = NAME + ":HIDE_CAMERA_ACCESS_DIALOG"; | |
public static const CAMERA_ACCESS_GRANTED : String = NAME + ":CAMERA_ACCESS_GRANTED"; | |
public static const CAMERA_ACCESS_DENIED : String = NAME + ":CAMERA_ACCESS_DENIED"; | |
public static const CAMERA_SEARCH_STARTED : String = NAME + ":CAMERA_SEARCH_STARTED"; | |
public static const CAMERA_SEARCH_PROGRESS : String = NAME + ":CAMERA_SEARCH_PROGRESS"; | |
public static const CAMERA_SEARCH_FINISHED : String = NAME + ":CAMERA_SEARCH_FINISHED"; | |
public static const CAMERA_FOUND : String = NAME + ":CAMERA_FOUND"; | |
public static const CAMERA_NOT_FOUND : String = NAME + ":CAMERA_NOT_FOUND"; | |
// | |
private static const CAMERA_STATUS_UNMUTED : String = "Camera.Unmuted"; | |
private static const CAMERA_STATUS_MUTED : String = "Camera.Muted"; | |
// | |
search static const TIMEOUT : int = 7000; | |
// | |
private var _stage : Stage; | |
private var _granted : Boolean; | |
private var _activeVideo : Video; | |
private var _activeCamera : Camera; | |
// | |
search var busy : Boolean; | |
search var timeout : TimeDelay; | |
search var camera : Camera; | |
search var video : Video; | |
search var stopWatch : Stopwatch; | |
search var progressBase : Number; | |
// | |
access var started : Boolean; | |
access var finished : Boolean; | |
access var camera : Camera; | |
access var video : Video; | |
public function WebCamProxy(inStage : Stage) { | |
super(NAME); | |
_stage = inStage; | |
} | |
/** | |
* @return whether the user has granted access to the camera. | |
*/ | |
public function get granted() : Boolean { | |
return _granted; | |
} | |
/** | |
* @return the Video object that has the active Camera attached to it after the searchAndActivate() process completes. | |
*/ | |
public function get activeVideo() : Video { | |
return search::video; | |
} | |
/** | |
* Activates the camera privacy check. This will end in one of these two notifications sent: | |
* #CAMERA_ACCESS_DENIED or #CAMERA_ACCESS_GRANTED | |
* | |
* In the process, these two notifications might get sent: | |
* #SHOW_CAMERA_ACCESS_DIALOG and #HIDE_CAMERA_ACCESS_DIALOG | |
* | |
* TODO: Figure out how to know if a user denied access and remembered the setting!!! | |
*/ | |
public function accessCamera() : void { | |
use namespace access; | |
if (started) { | |
if (finished) { | |
if (camera.muted) { | |
access::sendNotification(CAMERA_ACCESS_DENIED, false); | |
} else { | |
access::sendNotification(CAMERA_ACCESS_GRANTED, true); | |
} | |
} else { | |
// let the process finish first | |
} | |
return; | |
} | |
started = true; | |
camera = Camera.getCamera(); | |
if (!camera) { | |
access::sendNotification(CAMERA_ACCESS_DENIED, false); | |
return; | |
} | |
// If privacy settings are normal, attaching the camera will trigger the security dialog. | |
// The status event catches the result: | |
camera.addEventListener(StatusEvent.STATUS, handleStatusEvent); | |
if (!camera.muted) { | |
// If privacy settings are set to allow always, the muted property is already false | |
access::sendNotification(CAMERA_ACCESS_GRANTED, true); | |
return; | |
} | |
public::sendNotification(SHOW_CAMERA_ACCESS_DIALOG); | |
video = new Video(); | |
video.attachCamera(camera); | |
} | |
/** | |
* Activates the search process that will eventually send one of two possible notifications: | |
* #CAMERA_FOUND or #CAMERA_NOT_FOUND | |
* | |
* Upon success, the activated camera is added as the notification body, and you can get the running camera via #activeCamera(); | |
*/ | |
public function searchAndActivateCamera() : void { | |
use namespace search; | |
if (busy) { | |
warn("already searching, not starting twice."); | |
return; | |
} | |
busy = true; | |
stopWatch = new Stopwatch(); | |
if (_activeCamera) { | |
found(_activeCamera); | |
return; | |
} | |
sendNotification(CAMERA_SEARCH_STARTED); | |
tryCamera(); | |
} | |
public function deactivateCamera() : void { | |
if (!(_activeCamera && _activeVideo)) { | |
critical("How can this be called if there is no active camera nor video? Check the flow."); | |
return; | |
} | |
_activeVideo.attachCamera(null); | |
} | |
access function handleStatusEvent(e : StatusEvent) : void { | |
use namespace access; | |
public::sendNotification(HIDE_CAMERA_ACCESS_DIALOG); | |
switch (e.code) { | |
case CAMERA_STATUS_MUTED: | |
access::sendNotification(CAMERA_ACCESS_DENIED, false); | |
break; | |
case CAMERA_STATUS_UNMUTED: | |
access::sendNotification(CAMERA_ACCESS_GRANTED, true); | |
break; | |
} | |
} | |
access function sendNotification(inNotification : String, inGranted : Boolean) : void { | |
use namespace access; | |
_granted = inGranted; | |
if (camera) { | |
camera.removeEventListener(StatusEvent.STATUS, handleStatusEvent); | |
} | |
if (video) { | |
video.attachCamera(null); | |
video.clear(); | |
video = null; | |
} | |
finished = true; | |
info(inNotification); | |
super.sendNotification(inNotification); | |
} | |
search function tryCamera(inIndexList : Array = null) : void { | |
use namespace search; | |
if (!inIndexList) { | |
// The first time this method runs, there's no inIndex. | |
// that's when we initialize the Video object and try out the default camera. | |
search::video = new Video(); | |
camera = Camera.getCamera(); | |
} else { | |
camera.removeEventListener(ActivityEvent.ACTIVITY, handleActivity); | |
// get the camera at the current index | |
camera = Camera.getCamera(inIndexList.shift()); | |
} | |
if (!camera) { | |
// we've run out of camera's to choose from. | |
giveUp(); | |
return; | |
} | |
// We have a camera, we just don't know if it supplies an image | |
debug("Attempting " + (inIndexList ? "" : "default") + " camera: #" + camera.index + " --> " + camera.name); | |
// sync the framerate to the stage | |
camera.setMode(320, 240, _stage.frameRate); | |
// set the motion level and timeout so the camera fires only one activity event. | |
camera.setMotionLevel(1, 0); | |
camera.addEventListener(ActivityEvent.ACTIVITY, handleActivity); | |
// attach the camera (the first time this might trigger the access dialog). | |
search::video.attachCamera(camera); | |
if (!inIndexList) { | |
inIndexList = []; | |
// populate the list with all camera indexes, except for the one we just tried. | |
for (var i : int = 0;i < Camera.names.length;i++) { | |
if (i != camera.index) { | |
inIndexList.push(i); | |
} | |
} | |
} | |
// log the time for debugging | |
stopWatch.reset(); | |
stopWatch.addEventListener(TimerEvent.TIMER, sendProgress); | |
sendProgress(null, inIndexList.length + 1); | |
stopWatch.start(); | |
if (inIndexList.length) { | |
// start the inactivity time out to try out the next camera. | |
// if the timeout is not cancelled/reset within the given time, this method is called again | |
timeout = new TimeDelay(tryCamera, TIMEOUT, [inIndexList]); | |
} else { | |
timeout = new TimeDelay(giveUp, TIMEOUT); | |
} | |
} | |
search function sendProgress(e : Event = null, inCamerasLeft : Number = -1) : void { | |
use namespace search; | |
var chunk : Number = 1 / Camera.names.length; | |
if (inCamerasLeft >= 0) { | |
progressBase = Camera.names.length - inCamerasLeft; | |
} | |
var partial : Number = Math.min(stopWatch.time / TIMEOUT, 1); | |
var percentage : Number = chunk * (progressBase + partial); | |
sendNotification(CAMERA_SEARCH_PROGRESS, percentage); | |
} | |
search function giveUp() : void { | |
use namespace search; | |
busy = false; | |
error("No camera on this machine"); | |
sendNotification(WebCamProxy2.CAMERA_SEARCH_FINISHED); | |
sendNotification(CAMERA_NOT_FOUND); | |
} | |
search function handleActivity(event : ActivityEvent) : void { | |
use namespace search; | |
if (timeout) { | |
timeout.reset(); | |
} | |
var cam : Camera = (event.target) as Camera; | |
cam.removeEventListener(ActivityEvent.ACTIVITY, handleActivity); | |
found(cam); | |
} | |
search function found(inCamera : Camera) : void { | |
use namespace search; | |
busy = false; | |
_activeCamera = inCamera; | |
_activeVideo = video; | |
// if the camera has been deactivated, this will reactivate. | |
_activeVideo.attachCamera(_activeCamera); | |
sendNotification(CAMERA_SEARCH_FINISHED); | |
sendNotification(CAMERA_FOUND, _activeCamera); | |
info("Found camera at #" + _activeCamera.index + " --> " + _activeCamera.name + ", after " + stopWatch.stop() / 1000 + " seconds"); | |
} | |
} | |
} | |
/** | |
* Don't expose variables in these namespaces | |
*/ | |
namespace search; | |
namespace access; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment