1. 사전 지식
-
Blob
-
정의
- Blob 는 일반적으로 미디어(이미지, 사운드, 비디오) 파일과 같은 큰 용량의 파일을 말한다.
-
Blob Object
-
Blob Object 는 File 과 같은 불변 객체를 나타내며, raw data 를 가진다.
- 추가로 File 인터페이스 는 Blob 인터페이스 의 모든 특성들을 상속받는다.
-
-
-
JavaScript Typed Array
-
정의
-
Typed Array 는 raw binary data 에 접근하기 위한 방법을 제공한다.
- 즉 자바스크립트로 binary data 를 다루기 위해 사용한다.
-
유연성과 효율성을 위해 buffer 와 view 로 나눠 구현되어있다.
-
buffer
-
ArrayBuffer 는 고정된 크기의 raw binary data 를 나타내기 위해 사용된다.
-
ArrayBuffer 클래스 통해 생성된 buffer 는 데이터 청크를 나타내는 객체이다.
// 12 bytes buffer 나타낸다 var buffer = new ArrayBuffer(12);
-
buffer 는 저장된 데이터를 접근하기 위한 방법을 제공하지 않는다.
-
데이터를 다루기 위해서는 반드시 view 를 사용해야한다.
-
-
view
-
DataView
DataView view 는 buffer 에 저장된 데이터로 부터 값을 읽고, 쓰기 위한 low-level 인터페이스 를 제공한다.(getter/setter API 제공)
// 12 bytes byffer var buffer = new ArrayBuffer(12); // view 를 생성한다. var view = new DataView(buffer, 2, 2); // 해당 view 가 시작하는 위치를 반환한다. console.log(view.byteOffset); // 2
데이터를 다루기위한 DataView 의 특성들은 아래와 같다.
-
Typed Array Views
DataView 를 상속한 아래 클래스들을 통해 buffer 에 저장된 데이터를 다룰 수 있게된다.
Int8Array, Uint8Array, Int32Array, Uint32Array 등 …
위 클래스 중 Int32Array 를 통해 생성된 view 는 아래와 같은 특성들을 가지게된다.
아래 그림은 각 view 에 따라 나눠지는 메모리 공간을 나타낸다.
/* ArrayBuffer(20 bytes) 8bit == 1 byte ArrayBuffer / 1 byte = 20; */ var buffer = new ArrayBuffer(20); // 부호 없는 1 byte 정수 배열 var uint8View = new Uint8Array(buffer); console.log(uint8View.length); // 20 /* ArrayBuffer(20 bytes) 32bit == 4 byte ArrayBuffer / 4 byte = 5; */ var buffer = new ArrayBuffer(20); // 부호 없는 4 byte 정수 배열 var uint32View = new Uint32Array(buffer); console.log(uint32View.length); // 5
unsigned or signed view test
// unsigned int 8(1 bytes) var buffer = new ArrayBuffer(20); var uint8View = new Uint8Array(buffer); // 0 ~ 255(unsigned int 8(1 bytes) 로 표현 가능한 수) uint8View[0] = 0; uint8View[1] = 255; console.log(uint8View); // [0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // signed int 8(1 bytes) var buffer = new ArrayBuffer(20); var int8View = new Int8Array(buffer); // -127 ~ 128(signed int 8(1 bytes) 로 표현 가능한 수) // signed 의 경우 부호(양수/음수)를 나타내기 위해 총 8bit 중 1 비트(0: 양수, 1: 음수) 사용하기 때문에, 나머지 7bit(-127 ~ 128(표현 가능한 수))를 통해 숫자를 표현하게 된다. int8View[0] = -128; int8View[1] = 127; console.log(int8View); // [-128, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // unsigned int 16(2 bytes) var buffer = new ArrayBuffer(20); var uint16View = new Uint16Array(buffer); // 0 ~ 65535(unsigned int 16(2 bytes) 로 표현 가능한 수) uint16View[0] = 65535; console.log(uint16View); // [65535, 0, 0, 0, 0, 0, 0, 0, 0, 0]
-
-
-
-
-
Little-Endian or Big-Endian
-
정의
-
Endian 은 컴퓨터에서 데이터가 저장되는 순서(byte order)를 말한다.
-
정리:
-
메모리는 하위 주소에서 상위 주소로 데이터가 저장된다.
-
Little Endian: 하위 바이트부터 데이터가 저장되는 방식.
- Little Endian 방식의 장점: 산술연산유닛(ALU)에서 메모리를 읽는 방식이 메모리 주소가 낮은 쪽에서부터 높은 쪽으로 읽기 때문에 산술 연산의 수행이 더 쉽다.(연산 처리 과정에서 이런 장점이 있는 정도로만 알고 넘어가자…)
-
Big Endian: 상위 바이트부터 데이터가 저장되는 방식.
-
-
-
적용 이유:
- 각 CPU(Intel / Spac) 타입에 따라 차이를 보이는 byte order(데이터 저장 순서)는, 동일한 시스템 안에서만 데이터를 주고 받는다면, Endian 에 대해 전혀 신경쓸 필요가 없지만, 이기종간에 데이터를 주고 받을 경우, 서로간의 데이터 저장 방식 차이로 인해 전혀 엉뚱한 결과를 반환하게 된다.
-
서로 다른 Endian 간의 데이터 통신 해결책:
-
공통되는 Endian(약속된 Endian 규칙)으로 변환 후, 데이타를 주고/받는 방법.
- 즉 서로간에 사용할 Endian(Little Endian or Big Endian) 을 하나로 통일시켜 데이터를 주고 받는 것이다.
-
또 하나의 방법은 byte order(바이트 저장 순서) 를 신경쓸 필요가 없는, 데이터 타입을 사용하는 것이다. char 타입은 1byte 의 크기를 가지기때문에, byte order 에 대해 전혀 신경쓸 필요가 없다. 예를 들면 12345678 을 int 형으로 보내는 대신 문자열 “12345678” 로 변환시켜 전송하면 된다.
-
-
2. JavaScript 를 통해 Binary Data 조작하기
-
예제 소스에서는 NodeJS 및 Socket.IO 와 관련된 내용은 최대한 배제 하였습니다.(특별히 포스트 내용과 관련 없다고 판단한 내용)
-
파일 업로드
-
File 및 FileReader API 를 지원하는 브라우저를 통해 파일 업로드 기능을 만들 수 있다.
-
하지만 IE(10/11 포함) 브라우저는 지원하지 않는다고 보면된다.
-
Source Example
-
Cliend Side(use JS)
-
먼저 저장소로 부터 내려받은 파일을 include 한다.
<script src="siofu_client.js"></script>
// socket 서버에 연결 var socket = io.connect('http://localhost:9090'); // socket 객체를 SocketIOFileUpload 클래스로 전달한다. var uploader = new SocketIOFileUpload(socket); // listenOnSubmit 메서드에 input[type="button"] 및 input[type="file"] Element 를 전달한다. uploader.listenOnSubmit($('#btn_upload').get(0), $('#siofu_input').get(0)); // KiB === byte 단위 // KB === KByte 단위 // 한번에 로드될 chunks 파일 사이즈 // chunkSize 를 0으로 할당하면, chunk 를 사용하지 않게 된다. uploader.chunkSize = 1024 * 100; // 102400 byte 로 chunk 단위를 나눈다. uploader.addEventListener("start", function(event){ console.log('started upload of file'); }); // progress 이벤트를 통해 현재 진행 상황을 볼 수 있다. uploader.addEventListener("progress", function(event){ var percent = event.bytesLoaded / event.file.size * 100; console.log("File is", percent.toFixed(2), "percent loaded"); }); // 파일 업로드가 끝날을때 이벤트가 발생한다. uploader.addEventListener("complete", function(event){ console.log('completed file upload'); });
-
Server Side(use nodeJS)
var uploader = new siofu(); uploader.dir = "uploads"; uploader.listen(socket); // Do something when a file is saved: uploader.on("saved", function(event){ console.log(event.file); }); // Error handler: uploader.on("error", function(event){ console.log("Error from uploader", event); });
-
-
참고 사이트
-
-
-
이미지 효과
-
서버에서 내려받은 ArrayBuffer(이미지 데이터) 로 view(uInt8Array) 를 생성 후, 버퍼에 저장된 데이터를 조작한다.
-
Source Example
-
Cliend Side(use JS)
var cw = 327; var ch = 125; // canvas Element 를 가져온다. var canvas = document.querySelector('canvas'); // context 를 생성한다. var ctx = canvas.getContext('2d'); // view(부호 없는 1byte 정수 배열)를 생성한다. var uInt8Array = new Uint8Array(payload.buffer); // view를 통해 Blob Object 를 생성한다. var blob = new Blob([uInt8Array], {type: 'image/jpeg'}); var originalImgData = null; // Blob Object를 참조하는 URL를 생성한다. var url = URL.createObjectURL(blob); var img = new Image; // 이미지 로드 이벤트 $(img).bind('load', function(){ canvas.width = img.width; canvas.height = img.height; // 캔버스에 해당 이미지를 그린다. ctx.drawImage(img, 0, 0, img.width, img.height); // 각 px 에 대한 정보(r,g,b,a)가 담긴 이미지 데이터를 가져온다. originalImgData = ctx.getImageData(0, 0, canvas.width, canvas.height); // 반전 효과를 준다. // invert(); // 흑백 효과를 준다. empty(); }); // Blob 객체를 참조하는 URL을 img.src 에 할당 후 로드한다. img.src = url; // px 단위의 이미지 데이터를 조작하여, 반전 효과를 준다. function invert(){ originalImgData = ctx.getImageData(0, (canvas.height / 2), canvas.width, canvas.height); var data = originalImgData.data; for (var i = 0; i < data.length; i += 4) { data[i] = 255 - data[i]; // red data[i + 1] = 255 - data[i + 1]; // green data[i + 2] = 255 - data[i + 2]; // blue } ctx.putImageData(originalImgData, 0, (canvas.height / 2)); }; // px 단위의 이미지 데이터를 조작하여, 흑백 효과를 준다. function empty(){ originalImgData = ctx.getImageData(0, 0, canvas.width, canvas.height); var data = originalImgData.data; for (var i = 0; i < data.length; i += 4) { // 각 픽셀의 밝기만 조사하여 R, G, B 색상 요소를 균일하게 만들면 회색이 된다.(색상 정보를 아래 공식(각 요소(R, G, B)가 밝기에 미치는 영향은 29:58:11로 전문가에 의해 계산되어 있다)으로 R,G,B 요소에서 제거한다) // 128 이상은 흰색으로, 128 이하는 검정색으로 만들어 버림으로써, 흰색과 검정색 두 가지만 남긴다. 경계값인 128을 조정하면 밝기가 달라진다. var gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114; if (gray > 128){ gray = 255; } else{ gray = 0; } data[i] = gray; // red data[i + 1] = gray; // green data[i + 2] = gray; // blue } ctx.putImageData(originalImgData, 0, 0); };
-
Server Side(use nodeJS)
var fs = require('fs'); // 파일을 읽은 후 클라이언트로 버퍼를 전달한다. fs.readFile('./lib/img/nmms_20823487.jpg', function (err, buf) { // it's possible to embed binary data // within arbitrarily-complex objects socket.emit('onSocketMsg', { type: 'resultImageData', payload: { buffer: buf } }); });
-
-
적용 결과
-
원본 이미지
-
invert 함수 적용 이미지
-
empty 함수 적용 이미지
-
-
참고 사이트
-
-
ArrayBuffer 로 내려받은 비디오 플레이
-
서버에서 내려받은 ArrayBuffer(영상 데이터) 로 view(in Typed Array Views) 를 생성 후, 버퍼에 저장된 데이터를 조작한다.
-
영상 및 오디오 데이터의 경우, 브라우저 지원 여부 및 지원 포맷에 대해 반드시 확인해봐야한다.
-
아래 소스는 Chrome 브라우저에서
*
.mp4 및*
.webm 포맷으로만 테스트되었습니다.
-
-
Source Example
-
Cliend Side(use JS)
var vw = 327; var vh = 125; // video Element 를 가져온다. var video = document.querySelector('video'); video.width = vw; video.height = vh; // view(부호 없는 1byte 정수 배열)를 생성한다. var uInt8Array = new Uint8Array(payload.buffer); // view를 통해 Blob Object 를 생성한다. var blob = new Blob([uInt8Array], {type: 'video/webm'}); //var blob = new Blob([uInt8Array], {type: 'video/mp4'}); // Blob Object를 참조하는 URL 를 생성한다. var url = URL.createObjectURL(blob); // Blob 객체를 참조하는 URL을 video.src 에 할당 후 로드한다. video.src = url;
-
Server Side(use nodeJS)
fs.readFile('redcliff450.webm', function (err, buf) { socket.emit('onSocketMsg', { type: 'resultVideoData', payload: { buffer: buf } }); });
-
-
-
Chunk 방식으로 내려받은 비디오 플레이
-
서버에서 Chunk 방식으로 내려받은 ArrayBuffer(영상 데이터) 로 view(in Typed Array Views) 를 생성 후, 버퍼에 저장된 데이터를 조작한다.
-
MediaSource API 를 통해 내려받은 영상 데이터를 조작할 수 있다.
-
영상 및 오디오 데이터의 경우, 브라우저 지원 여부 및 지원 포맷에 대해 반드시 확인해봐야한다.
-
아래 소스는 Chrome 브라우저에서
*
.webm(vorbis 및 vp8 코덱) 포맷으로만 테스트되었습니다. -
Source Example
-
Cliend Side(use JS)
-
저장소로 부터 내려받은 파일을 include 한다.
<script src="socket.io-stream.js"></script>
// stream 메서드에 socket 객체를 전달 후 해당 이벤트를 바인딩한다. ss(socket).on('onSocketMsg', function(data) { data = data || {}; var type = data.type; var payload = data.payload; if (type === 'resultChunkVideoData') { var vw = 1024; var vh = 768; // video Element 를 가져온다. var video = document.querySelector('video'); video.width = vw; video.height = vh; console.log(payload.stream); // 내려받은 stream 데이터 // MediaSource 객체를 생성한다. var ms = new MediaSource(); // MediaSource 객체를 참조하는 URL 를 생성한다. var url = URL.createObjectURL(ms); // MediaSource 객체를 참조하는 URL을 video.src 에 할당 후 로드한다. video.src = url; // MediaSource 객체에 각 이벤트를 바인딩 시킨다. ms.addEventListener('sourceopen', callback, false); // ms.addEventListener('webkitsourceopen', callback, false); ms.addEventListener('sourceended', function(e) { console.log('mediaSource readyState: ' + this.readyState); }, false); function callback() { // 재생하려는 영상 소스를 추가한다. var sourceBuffer = ms.addSourceBuffer('video/webm; codecs="vp8"'); // var sourceBuffer = ms.addSourceBuffer('video/webm; codecs="vp8,vorbis"'); sourceBuffer.addEventListener('updatestart', function (e) { // console.log('updatestart: ' + ms.readyState); }); sourceBuffer.addEventListener('update', function () { // console.log('update: ' + ms.readyState); }, false); sourceBuffer.addEventListener('updateend', function (e) { console.log('updateend: ' + ms.readyState); }); sourceBuffer.addEventListener('error', function (e) { console.log('error: ' + ms.readyState); }); sourceBuffer.addEventListener('abort', function (e) { console.log('abort: ' + ms.readyState); }); payload.stream.on('data', function (data) { // chunk data console.log(data); // 버퍼에 내려받은 스트림 데이터를 할당한다. sourceBuffer.appendBuffer(data); }); // 데이터 전송이 완료되었을 경우 발생한다. payload.stream.on('end', function () { console.log('endOfStream call'); // 스트림을 종료한다. ms.endOfStream(); }); } } });
-
Server Side(use nodeJS)
var ss = require('socket.io-stream'); ss(socket).on('onSocketMsg', function(data) { data = data || {}; var type = data.type; var payload = data.payload; var stream = ss.createStream(); if (type === 'downloadChunkVideo') { // webm 포맷의 영상을 가져온다. var filename = path.basename('feelings_vp9-20130806-244.webm'); // 파일 스트림을 생성한다. fs.createReadStream(filename).pipe(stream); ss(socket).emit('onSocketMsg', { type: 'resultChunkVideoData', payload: { stream: stream } }); } });
-
테스트 결과
-
-
참고 사이트
-