JavaScript 를 통해 Binary Data 조작하기

August 31, 2015

1. 사전 지식

  • Blob

    • 정의

      • Blob 는 일반적으로 미디어(이미지, 사운드, 비디오) 파일과 같은 큰 용량의 파일을 말한다.
    • Blob Object

      • Blob Object 는 File 과 같은 불변 객체를 나타내며, raw data 를 가진다.

        • 추가로 File 인터페이스Blob 인터페이스 의 모든 특성들을 상속받는다.

    • Blob API in MDN

    • Blob in Terms

    • Blob in Can I Use

  • JavaScript Typed Array

    • 정의

      • Typed Array 는 raw binary data 에 접근하기 위한 방법을 제공한다.

        • 즉 자바스크립트로 binary data 를 다루기 위해 사용한다.
      • 유연성효율성을 위해 bufferview 로 나눠 구현되어있다.

        • buffer

          • ArrayBuffer 는 고정된 크기의 raw binary data 를 나타내기 위해 사용된다.

          • ArrayBuffer 클래스 통해 생성된 buffer데이터 청크를 나타내는 객체이다.

            // 12 bytes buffer 나타낸다
            var buffer = new ArrayBuffer(12);
          • buffer저장된 데이터를 접근하기 위한 방법을 제공하지 않는다.

          • 데이터를 다루기 위해서는 반드시 view 를 사용해야한다.

        • view

          • DataView

            DataView viewbuffer 에 저장된 데이터로 부터 값을 읽고, 쓰기 위한 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]
      • ArrayBuffer

      • DataView

      • DataView API Test

      • JavaScript Typed Arrays

      • JavaScript Typed Arrays(번역)

      • signed 와 unsigned 의 차이

      • signed 가 음수 표현하는 방법(상세 설명)

      • 나는 unsigned 가 싫어요

      • signed or unsigned 자료형의 범위

      • Binary Convert

  • 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” 로 변환시켜 전송하면 된다.

    • 엔디언

    • Little-Endian or Big-Endian(개념 잡기 좋은 문서)

    • Little-Endian or Big-Endian 개념

    • Endian 에 대해서

2. JavaScript 를 통해 Binary Data 조작하기

  • 예제 소스에서는 NodeJSSocket.IO 와 관련된 내용은 최대한 배제 하였습니다.(특별히 포스트 내용과 관련 없다고 판단한 내용)

  • 파일 업로드

    • FileFileReader 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);
        	});
      • 참고 사이트

        socket io file upload module

        File API

        FileReader API

  • 이미지 효과

    • 서버에서 내려받은 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
        				}
        			});
        		}
        	});
      • 테스트 결과

    • 참고 사이트


Profile picture

Written by mohwa