2007年8月10日星期五

Flash WebCam 拍照存檔

要在 Flash 中,使用 WebCam 拍照,僅需透過 BitmapData 即可擷取畫面,然後問題在於如何將這張畫面存成檔案?由 Server 程式來做?Flash 如何將資料傳給 Server 程式?在 Flash 8 的時候,我曾經將 BitmapData 的所有像素讀出來串成一大串的字串送給後端,現在,則有更容易的作法了。

有人用 AS3 寫好 Flash 版本的 PNG / JPG encoder,可以將 BitmapData 變成 PNG / JPG 格式的 ByteArray,然後只要我們能將 ByteArray 送到後端程式作儲存即可。資料傳送的部份也有人是利用 URLRequest / URLLoader 來傳送 stream 資料到後端,然後由 PHP / ASP / Java 接收儲存。更讚的是,有人寫好一個 function 可以直接將 ByteArray 資料打包成 HTTP upload 的規格,這樣一來後台程式就跟接收一般由 HTML 標籤所做的上傳程序沒啥不同了。



首先,我的後端,是以 Java Servlet 開發,順便寫一個 index.html 來測試這個 Java Servlet 的正確性:


<HTML>

<p>Demo MultipartRequest Upload Servlet</p>
<FORM ACTION="FileUpload" METHOD=POST ENCTYPE="multipart/form-data">
What is your name? <INPUT TYPE=TEXT NAME=submitter> <BR>
Which file to upload? <INPUT TYPE=FILE NAME=file1> <BR>
Which file to upload? <INPUT TYPE=FILE NAME=file2> <BR>
Which file to upload? <INPUT TYPE=FILE NAME=file3> <BR>
<INPUT TYPE=SUBMIT>
</FORM>

</HTML>


然後,Java Servlet 的部份,有利用到 com.oreilly.servlet 的上傳類別 (http://www.servlets.com/cos/) 來處理:


package idv.ben.fileUpload;

import java.io.*;
import java.util.Enumeration;

import javax.servlet.*;
import javax.servlet.http.*;

import com.oreilly.servlet.MultipartRequest;

public class FileUpload extends javax.servlet.http.HttpServlet implements
javax.servlet.Servlet {

static final long serialVersionUID = 1L;

private String dirName;

public void init(ServletConfig config) throws ServletException {
super.init(config);
dirName = config.getInitParameter("uploadDir");
if (dirName == null) {
throw new ServletException("Please supply uploadDir parameter");
}
}

protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}

protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
response.setContentType("text/plain");
out.println("Demo Upload Servlet using MultipartRequest");
out.println();

try {
MultipartRequest multi = new MultipartRequest(request, dirName,
10 * 1024 * 1024, "ISO-8859-1");

out.println("PARAMS:");
Enumeration params = multi.getParameterNames();
while (params.hasMoreElements()) {
String name = (String) params.nextElement();
String value = multi.getParameter(name);
out.println(name + "=" + value);
}
out.println();

out.println("FILES:");
Enumeration files = multi.getFileNames();
while (files.hasMoreElements()) {
String name = (String) files.nextElement();
String filename = multi.getFilesystemName(name);
String originalFilename = multi.getOriginalFileName(name);
String type = multi.getContentType(name);
File f = multi.getFile(name);
out.println("name: " + name);
out.println("filename: " + filename);
out.println("originalFilename: " + originalFilename);
out.println("type: " + type);
if (f != null) {
out.println("f.toString(): " + f.toString());
out.println("f.getName(): " + f.getName());
out.println("f.exists(): " + f.exists());
out.println("f.length(): " + f.length());
}
out.println();
}
} catch (IOException lEx) {
this.getServletContext().log(lEx, "error reading or saving file");
}
}
}


另外,也稍微注意一下 web.xml 的部份,有將存檔資料夾設定在 servlet 的初始化參數中:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>
FileUpload</display-name>
<servlet>
<description>
</description>
<display-name>
FileUpload</display-name>
<servlet-name>FileUpload</servlet-name>
<servlet-class>
idv.ben.fileUpload.FileUpload</servlet-class>
<init-param>
<param-name>uploadDir</param-name>
<!-- Current dir for the server process, replace with /tmp or c:\temp maybe -->
<param-value>C:\temp</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>FileUpload</servlet-name>
<url-pattern>/FileUpload</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
</web-app>


以上,其實沒啥特別的,這跟一般的接收上傳的程式沒啥兩樣,若是你用 PHP 或其他語言的話,就根本不用理會上面這段。

接下來才是重點,我以 Flex 的 MXML 專案來開發這個 WebCam 拍照介面,並撰寫傳送資料到後台的程式:


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="onAppInit()">
<mx:Script>
<![CDATA[
import mx.core.UIComponent;

import net.kaourantin.JPEGEncoder;
import com.jooce.net.uploadFile;

private var camera:Camera;
private var video:Video;
private var bd:BitmapData;

private function onAppInit():void{
camera = Camera.getCamera();
if(camera!=null){
video = new Video(320, 240);
video.attachCamera(camera);

var holder:UIComponent = new UIComponent();
holder.addChild(video);
panelCamera.addChild(holder);
}
}

private function takePhoto():void{
if(video!=null){
bd = new BitmapData(320, 240);
var bmp:Bitmap = new Bitmap(bd);

var holder:UIComponent = new UIComponent();
holder.addChild(bmp);
panelPhoto.addChild(holder);

bd.draw(video);
}
}

private function uploadPhoto():void{
if(bd!=null){
var jpegEnc:JPEGEncoder = new JPEGEncoder(75);
var jpegDat:ByteArray = jpegEnc.encode(bd);

var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onURLLoaderComplete);
loader.addEventListener(IOErrorEvent.IO_ERROR, onURLLoaderFailure);

uploadFile(loader, 'http://localhost:9001/FileUpload/FileUpload', jpegDat, 'photo.jpg');
}
}

private function onURLLoaderComplete(evt:Event):void{
trace("onURLLoaderComplete");
}

private function onURLLoaderFailure(evt:IOErrorEvent):void{
trace("onURLLoaderFailure");
}

]]>
</mx:Script>
<mx:Panel width="340" height="280" layout="absolute" id="panelCamera" left="10" top="10" title="Camera">
</mx:Panel>
<mx:Panel width="340" height="280" layout="absolute" id="panelPhoto" top="10" left="360" title="Photo">
</mx:Panel>
<mx:Button label="Take Photo" id="btnTakePhoto" left="260" top="300" click="takePhoto()"/>
<mx:Button label="Upload Photo" id="btnUploadPhoto" left="600" top="300" click="uploadPhoto()"/>
</mx:Application>


不難看出,重點在於 JPEGEncoder 與 uploadFile。

JPEGEncoder 可以參考以下網址:

http://www.kaourantin.net/2005/10/more-fun-with-image-formats-in-as3.html
http://www.yov408.com/html/codespot.php?gg=47
http://www.kaourantin.net/source/JPEGEncoder.as

不過要小心的是,因為作者開發這個類別的時間比較早,所以有些部份可能會需要作一點點微調,像是 Flex 好像不允許一個 *.as 中有兩個 class,以及 flash.util-->flash.utils,與 Void-->void 等等。

另外,uploadFile() 的部份,可以參考以下網址:

http://www.jooce.com/blog/?p=143
http://www.jooce.com/blog/wp%2Dcontent/uploads/2007/06/uploadFile.txt

作者是寫成 package level 的 function,這個小地方稍微注意一下即可,用起來倒是還挺方便的。

16 意見:

milkmidi 提到...

謝謝分享
我的作法一樣是用AS3 JPGEncode
只是用php和amfphp來接值
來試試你的方法好了..3Q

max771122 提到...

你好邦邦,我正在做Flash WebCam 拍照存檔后台是php.但是我不是个flash高手.你能不能把你的软体Flash WebCam 发给我分享一下.先谢谢了
我的邮箱maxime_fr@hotmail.com

Ben Chang 提到...

抱歉,這個範例我是用 FLEX 開發的,完整原始檔都在文章中,你可以自由取用這篇文章中的資料!

匿名 提到...

邦邦老師您好~
我是AS3的新手~
請教一個問題~怎麼讓視訊讀到的畫質變好呢?我看了一下~FLASH CS3 中的說明~沒有提到相關的問題耶~?麻煩您指導一下~ 謝謝

匿名 提到...

哈~邦邦老師我是樓上的~~
我找到myCam.setMode()的方法了~
哈~學藝不精~~~我會好好努力的~~
邦邦老師~這裡真是個好地方~~~學到很多東西~^^加油~

游墨 提到...

邦邦老師 有一個問題外的問題

最近剛開始用Blogger的系統

關於你文章裡面那些程式碼的區塊要怎麼弄

是單純的<div>或<span>嘛
還是是用表格然後將一些保留的字元
例如: <  → <

最近在使用的時候出現了一些問題
這個系統好像很方便又好像跟我以前台灣一些Blog服務者的介面設計的習慣有很大的不同.

游墨 提到...

< → & l t ;

匿名 提到...

請問一下..
我在As3中要使用JPGEncode時..編譯都無法成功..
都會說變數重覆宣告..
不過我看log...那些所謂的重覆宣告的變數..大多是for迴圈中的i耶..
有什麼地方要修正的嗎?

Ben Chang 提到...

那就不要重複宣告啊!

var i:Number

for(i=0; i<5; i++){
trace(i)
}

for(i=5; i<10; i++){
trace(i)
}

Ben Chang 提到...

如果有網友的平台跟我 demo 的不一樣,我相信一定會的(因為 Java 有點硬,Flex 也是!)。

譬如你若是用 PHP 當作後台的話,只要你先學會如何用 PHP 寫上傳檔案的接收程式即可,像我上面的例子中,也至少先寫好 html 前端與 Java 後端,然後才將前端改用 flash。

若是你的前端不使用 flex 而是使用 flash ide 開發的話,那麼只要注意我程式中的 takePhoto() 將畫面存成 BitmapData,以及 uploadPhoto() 透過 JPEGEncoder 將 BitmapData 轉成 ByteArray,然後用 URLLoader 將 ByteArray 送給後台的上傳檔案接收程式即可。

阿祖 提到...

邦邦前輩您好:
我照你的方法做,不過我因為不用存檔,單單只需要照相定格的動作,所以我把動作都放到一個按鈕的函式裡,可是很奇怪的是,為什麼照下來後,會變得很小張然後顯示在同塊區域的左上角,是個像素有關嗎?!我該怎麼修改呢?
不好意思,新手剛接觸,問題比較多,還請前輩多多包涵,謝謝~

Ben Chang 提到...

可以注意一下 matrix 的用法

在製作BitmapData前,進行了mc的縮放
http://blog.xuite.net/ben19770209/ASBeginner/5255724

matrix在mc的beginBitmapFill之中的用處
http://blog.xuite.net/ben19770209/ASBeginner/5258900

因為,有可能你的原圖本來是小圖,卻被放大呈獻,所以截圖的時候,會抓到他原本的圖片大小,也就是小圖~這時候就可以透過 matrix 來控制截圖的縮放,或是簡單一點的話,你的原圖外面多加一層 sprite / mc,這樣你要截圖時就抓外面這個 sprite / mc 即可。

總之,原圖的縮放,對截圖出來的結果是不會有影響的,這反而使得開發者必須將截圖結果作些調整以配合"原圖縮放後的呈現"。

阿祖 提到...

邦邦前輩好:謝謝您給我的指教,正在研究怎麼用。不過我剛剛突然發現一個快速照相的方法,就是如果在不需要存檔的情況下,其實不用設BitmapData和Bitmap,只要再按鈕的地方,寫上vid.attachCamera(null);視訊影像就會停在那格了,這算是比較投機的方法吧??

無論如何,先謝謝前輩忙中抽空指教,小弟受惠了~

匿名 提到...

說到工作~我知道奧美、AGENDA安捷達 都很缺flash技術人員,至於薪資上,建議你把你分享多年blog上flash技術問題list給他們看看~還有那磨多同行的朋友們,相信你的身價不凡~
不過,廣告公司的creative directer或AD都只會平面設計,非常渴望flash專才卻也對flash技術人的專精與否很無知...
good luck~

Linus 提到...

您好,邦邦,看了你這篇文章讓我受用不少,可是我有一個大疑問想請問您,這個範例是儲存上傳單張照片,如果是要一次儲存多張照片呢?我嘗試用一個array去把每次轉出來的bytearray放進去,一開始似乎是可行的,可是大概跑了60張圖後就出現一個"script 已經執行超過預設的 15 秒逾時時段"的錯誤訊息,請問有更好得方法嗎?還是非得一張一張的將bytearray上傳呢?

Ben Chang 提到...

可以找一個做 zip 的 as3 library 試試看~

關於我






* ben {dot} chang {at} ben {dot} idv {dot} tw
* FriendFeed

贊助我1元美金:

Plurk

標籤雲