Rev. 2.73

이전 포스트에서 ArrayBuffer에 대해 알아보았습니다. 일반적으로 컴퓨팅에서 말하는 블랍(BLOB)은 대체로 커다란 파일을 일컫는 말이며, 사운드, 비디오와 같은 멀티미디어 데이터를 객체로 다루기 위해 주로 사용되는 것으로 알려졌습니다. File API가 생겨나면서 동시에 등장한 BLOB은 자바스크립트에서 조금 다른 의미로 해석되는데 지금은 파일을 다루기 위한 메모리 참조 수단 정도입니다.

원래 BLOB 객체는 스트림을 이용해서 읽기/쓰기를 하는 것이 정설이지만 자바스크립트에서는 바이너리로 수신한 청크(Chunk)들을 한방에 생성하여 File로 인식하는 용도로 자주 사용되며, FileReader API와 함께 사용하여 자바스크립트로 버퍼링을 제어하는 수준의 미디어 스트리밍을 구현할 수 있게 합니다. 또한, 이를 DOM으로 연결하기 위한 URL.createObjectURL이 존재합니다. 다음 예제는 AJAX에서 BLOB 데이터 형식으로 수신한 이미지를 DOM에 참조시키고 이미지가 출력된 후 URL.revokeObjectURL을 이용하여 BLOB을 해제하는 것입니다.

var req = new XMLHttpReqest();
xhr.open("GET", "download?name=" + name, true);
xhr.responseType = "blob";
xhr.onreadystatechange = function () {
  if (xhr.readyState == xhr.DONE) {
    var blob = xhr.reponse;
    var image = document.getElementById("my-image");
    image.addEventListener("load", function (evt) {
      URL.revokeObjectURL(evt.target.src);
    }
    image.src = URL.createObjectURL(blob);
  }
}
xhr.send();

Node.js의 BinaryJS모듈을 이용하여 스트림으로 수신한 Chunk를 배열로 취합하고 스트림이 끝나는 시점에 Blob으로 변환하여 DOM으로 참조시키는 예제:

// Connect to Binary.js server
var client = new BinaryClient('ws://localhost:9000');
// Received new stream from server!
client.on('stream', function(stream, meta){    
  // Buffer for parts
  var parts = [];
  // Got new data
  stream.on('data', function(arrayBuffer){
    parts.push(arrayBuffer);
  });
  stream.on('end', function(){
    // Display new data in browser!
    var img = document.createElement("img");
    img.src = URL.createObjectURL(new Blob(parts));
    document.body.appendChild(img);
  });
});

다음 예제는 MediaSource API와 연계하여 작동하는 것을 가정한 코드로서 현재 구글 크롬 브라우저에서 구현 중인 명세이고 WebM 포맷(vorbis, vp8 인코딩)으로 만들어진 미디어만 정상적으로 작동(웹 소켓 바이너리 통신)하는 것을 직접 확인한 것입니다. 처음으로 수신한 Chunk의 Blob 객체를 생성하고 FileReader API에서 뒤이어 수신하는 Chunk를 추가(Append)하여 미디어 스트리밍을 구현한 예제입니다.

var xhr = new XMLHttpRequest();
xhr.open('GET', 'test.webm', true);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
  if (xhr.status != 200) {
    alert("Unexpected status code " + xhr.status + " for " + url);
    return false;
  }
  callback(new Uint8Array(xhr.response));
};
xhr.send();

function callback(uInt8Array) {
  var file = new Blob([uInt8Array], {type: 'video/webm'});
  var chunkSize = Math.ceil(file.size / NUM_CHUNKS);
  console.log('num chunks:' + NUM_CHUNKS);
  console.log('chunkSize:' + chunkSize + ', totalSize:' + file.size);
  // Slice the video into NUM_CHUNKS and append each to the media element.
  var i = 0;
  (function readChunk_(i) {
    var reader = new FileReader();
    // Reads aren't guaranteed to finish in the same order they're started in,
    // so we need to read + append the next chunk after the previous reader
    // is done (onload is fired).
    reader.onload = function(e) {
      sourceBuffer.append(new Uint8Array(e.target.result));
      console.log('appending chunk:' + i);
      if (i == NUM_CHUNKS - 1) {
        mediaSource.endOfStream();
      } else {
        if (video.paused) {
          video.play(); // Start playing after 1st chunk is appended.
        }
        readChunk_(++i);
      }
    };
    var startByte = chunkSize * i;
    var chunk = file.slice(startByte, startByte + chunkSize);
    reader.readAsArrayBuffer(chunk);
  })(i); // Start the recursive call by self calling.
}

여기에 서버에서 재생시간 대비 클라이언트의 수신 속도를 측정하여 패킷을 절약하는 작업만 추가로 해주면 레알 미디어 스트리밍이 구현되는 것입니다.

Comments

최근 자바스크립트로 바이너리를 다룰 일이 생겨서 이런저런 삽질 중에 ArrayBuffer API는 짚고 넘어가야겠다는 생각이 들어 학습차 정리합니다. HTML5에서 지원하는 ArrayBuffer를 이용하여 Ajax 또는 WebSocket을 통해 바이너리 데이터를 서버와 브라우저 간에 송/수신할 수 있습니다. 이를테면, 서버와 클라이언트에서 Base64로 인코딩/디코딩하는 비용을 줄이거나 사진에서 EXIF 정보를 뽑아내거나, MediaSource API를 함께 사용하여 동영상 스트리밍을 구현할 수도 있으며, WebSocket으로 파일을 업로드하는 등 다양하게 활용할 수 있습니다.

일반적인 스트림(Stream) 객체에 의해 순차적으로 발생하는 이벤트로 전달되는 ArrayBuffer는 우리가 소위 말하는 Chunk 데이터이며 메모리에 위치하게 됩니다. 이것을 자바스크립트에서 다룰 수 있도록 하기 위해서 타입 배열 뷰(Typed array views)로 만들 수 있는데, 이는 ArrayBufferView 클래스로 생성할 수 있으며 그 종류는 다음과 같습니다.

// Signed integer arrays.
var i8 = new Int8Array(64)             // 1 byte,  8-bit twos complement signed integer
var i16 = new Int16Array(32)           // 2 bytes, 16-bit twos complement signed integer
var i32 = new Int32Array(16)           // 4 bytes, 32-bit twos complement signed integer

// Unsigned integer arrays.
var u8 = new Uint8Array(64)            // 1 byte,  8-bit unsigned integer
var u16 = new Uint16Array(32)          // 2 bytes, 16-bit unsigned integer
var u32 = new Uint32Array(16)          // 4 bytes, 32-bit twos complement signed integer
var pixels = new Uint8ClampedArray(64) // 1 byte,  8-bit unsigned integer

// Floating point arrays.
var f32 = new Float32Array(16)         // 4 bytes, 32-bit IEEE floating point number
var f64 = new Float64Array(8)          // 8 bytes, 64-bit IEEE floating point number

멋모르고 프로그래밍 세계에 발을 들여 놓은 저로서는(보는 것 만으로도 화가 남) 이렇게 많은 형식이 어디에 어떻게 쓰이는지 막연하기만 했는데, O'Reilly에서 출판한 Javascript: The Definitive Guide(자바스크립트 완벽 가이드) 6판 챕터 22에 뭔가 감을 잡을 수 있는 예문(문제 되면 삭제하겠습니다)이 있더군요.

var matrix = new Float64Array(9);   // A 3x3 matrix
var 3dPoint = new Int16Array(3);    // A point in 3D space
var rgba = new Uint8Array(4);       // A 4-byte RGBA pixel value
var sudoku = new Uint8Array(81);    // A 9x9 sudoku board

이것은 그냥 처리할 바이너리가 메모리에서 비트 연산에 효율적으로 사용될 유형을 선택해 주면 되는 정도로 어렴풋이 이해했습니다. 더 많은 내공이 쌓여야 이 타입들에 대한 개념을 탑재할 수 있을 듯합니다. ArrayBuffer를 다루는 더 쉬운 방법은 DataViewStringView를 이용하는 것입니다. DataView는 ArrayBuffer로부터 값을 읽거나 쓸 수 있도록 로우 레벨의 인터페이스를 제공하며, StringView는 문자열에 대한 C 스타일의 인터페이스를 제공합니다. 끝으로 준비한 예제는 비동기로 수신한 바이너리를 자바스크립트에서 사용할 수 있는 배열로 변환하여 어딘가에 써먹는 예제입니다.

var req = new XMLHttpRequest();
req.open('GET', "/your/binary/data");
req.responseType = "arraybuffer";
req.onload = function () {
    if (req.status != 200) {
        alert("Unexpected status code " + req.status);
        return false;
    }
    var buffer = req.response;
    var dataView = new DataView(buffer);
    var vectorLength = dataView.getUint8(0);
    var width = dataView.getUint16(1); // 0 + uint8 = 1 bytes offset
    var height = dataView.getUint16(3); // 0 + uint8 + uint16 = 3 bytes offset
    var vectors = new Float32Array(width * height * vectorLength);
    for (var i = 0, off = 5; i < vectors.length; i++, off += 4) {
        vectors[i] = dataView.getFloat32(off);
    }
    ...
}
req.send();

참고로 C언어에서는 다음과 같이 타입(Typedefs)을 선언합니다.

typedef unsigned char       uint8 // 8 bit unsigned integer
typedef          char        int8 // 8 bit signed integer 
typedef unsigned short     uint16 // 16 bit unsigned integer 
typedef          short      int16 // 16 bit signed integer 
typedef unsigned int  	   uint32 // 32 bit unsigned integer 
typedef          int        int32 // 32 bit signed integer 
typedef unsigned long 	   uint32 // 32 bit unsigned integer 
typedef          long       int32 // 32 bit signed integer

Comments

rsupport.png

다니던 회사에서 뻘짓만 하다가 팀에 불화가 생겨 그만두고 나왔습니다. 디아블로3에 미쳐 개폐인 생활을 하는 중에 알서포트 대표님께 러브콜이 왔습니다. 웹 기반 원격제어를 만들어보라고 하시네요? 게임도 슬슬 질려가고 뭔가 재미있을 것 같아서 복귀하기로 마음먹었습니다.

데모만 죽어라 만들어 대면서 실제 구현에 목말라 있었는데, 뭔가 엄청 바빠질 것 같은 느낌이 드는군요. 앞으로는 좋은일만 가득하기를….

Comments