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

Countly는 4인으로 구성된 터키출신의 착한 젊은 친구들에 의해 만들어진 실시간 모바일 앱 분석 도구입니다. 모바일 앱 엔드-유저의 행동 자료를 수집하고 분석하여 시각화해 줍니다. 구글 Analytics와 같은 웹 분석 도구를 모바일 앱에 최적화한 것으로 이해할 수 있겠네요. 오픈 소스이며 데이터 수집 및 관리할 수 있는 서버와 안드로이드, 윈도폰, iOS, 블랙베리(WebWorks)용 SDK를 동시에 제공합니다. (서드-파티에서 만든 유니티, 앱셀러레이터 타이타니움, 맥OSX용 SDK도 있습니다.) 재미있는 것은 SDK에서 제공하는 사용자 이벤트 API를 이용하면 앱 안에서의 사용자 패턴도 분석할 수 있습니다. (예를 들면, 게임 내 어떤 무기가 빈번하게 사용되는지 라던가...)

attachment

그들이 제공하는 데모를 실행해 보고 가지고 싶다는 생각이 들더군요. 나중에 서비스할지는 모르겠습니다만, 앱을 유료로 등록하고 사용할 수 있게 하는 기능은 없습니다. 그래서 Countly를 지금 사용해 보기 위해서는 운영 가능한 웹서버에 설치를 해야 하는 상황인 거죠. 서버는 Node.js와 MongoDB로 구축되었고 NginX와 Supervisor, Python 등의 패키지 설치를 요구하며, 리눅스 머신(우분투 권장)에 설치할 수 있습니다. 자체적으로 제공하는 설치 스크립트를 이용하면 Node.js를 비롯한 서버 실행 환경을 자동으로 구성해 줍니다. (비추천) 서버는 API와 Frontend로 양분되었으며, API 서버는 데이터의 입/출력을 담당하고, Express.js기반의 Frontend 서버는 데이터의 섹시한 비주얼라이제이션과 사용자 관리 기능을 포함합니다. 이 두 서버는 supervisord으로 관리되며, NginX에서 라우팅 룰에의해 서로 다른 포트를 가진 서버 프로세스로 프락시 패스하도록 구성되어 있었습니다. Cafe24에 놀리고 있는 가상서버에 설치하기 위해, 반나잘 삽질하고 윤진군의 도움으로 성공적으로 실행했습니다. 그런데 아쉽게도 등록할 만한 앱이 없네요. 혹시 관심 있는 분은 무료로 앱을 등록해 드릴께요. 요청에 한해서 등록한 앱 단위로 데이터베이스까지 덤프해서 드릴 생각입니다.

아쉽게도 한글 지원이 없어서 직접 한글화 작업을 완료하고 적용해 줄 것을 요청한 상태입니다. 그리고 각종 데이터를 PDF 문서로 생성해 주는 기능과 모바일에서 액세스할 수 있는 네이티브 앱을 개발 중이며 곧 선보일 예정이라고 하네요. 아주 멋집니다!

Comments