WebRTC, MQTT, Flutter

WebRTC를 이용한 화상/데이터 통신은 기본적으로 시그널 서버가 필요하다. 일반적으로 Websocket을 많이 사용하는데 서버는 클라이언트에서 전송되는 SDP, Candidate를 릴레이 하여 클라이언트가 연결할 상대방의 IP와 Port 정보를 주고 받은 다음 받은 정보를 이용하여 P2p 접속을 시도한다.

MQTT

MQTT는 발행/구독 기반으로 일대일, 일대다 데이터 통신에 적합하고 구독 채널을 트리구조로 구성 할 수 있기 때문에 Websocket에서 채팅방-서브 채팅방을 구현하는 기능을 아주 간단하게 구현 할 수 있다.

아이디어 – Mosquitto

WebRTC 화상통신앱을 만들며 처음부터 이 생각을 했다. WebSocket으로 채팅방을 만들지 않고 MQTT를 이용하면 안될까? Mosquitto 서버라면 보안성 문제도 쉽게 해결 할 수 있고 성능도 우수한 시그널 서버로 활용할 수 있지 않을까? 그래서 한번 해봤다.

Sample Code – Flutter

이 이미지는 대체 속성이 비어있습니다. 그 파일 이름은 1638688086230-1.jpg입니다

아래 링크의 소스 코드는 현재 만들고 있는 CCTV관련앱에서 기본적인 기능만 추출해서 Sample Project를 만들었다. Flutter로 만들어진 오픈소스이며 오픈된 Mosquitto 서버에 접속하여 동일한 토픽을 발행/구독하여 시그널을 주고 받은후 P2p를 연결하여 화상통신을 실행한다.

결론적으로 이 프로젝트에서 WebRTC 통신을 하기 위한 시그널 서버는 만들지않았다.

소스다운로드 링크

https://github.com/bipark/webrtc_mqtt_flutter_phone

P2p연결을 위해 Google Stun 서버를 사용하며 3G, LTE에서 연결이 되지 않을수도 있다. Stun 서버를 통한 P2p 연결이 불가능한 경우는 Turn 서버를 개별적으로 설치하고 운영해야 하는데 오픈소스인 Coturn을 활용하여 운용이 가능하다.

class MQTTManager {
  MqttServerClient? _client;

  MQTTManager(String clientId) {
    _client = MqttServerClient("test.mosquitto.org", clientId);
    _client!.autoReconnect = true;
    _client!.logging(on: false);
    _client!.keepAlivePeriod = 20;
  }

  Future<void> connect(String topic) async {
    try {
      await _client!.connect();
    } on Exception catch (e) {
      print('EXAMPLE::client exception - $e');
    }
    _client!.subscribe(topic, MqttQos.atMostOnce);
  }

  void publishMessage(String topic ,String message) {
    try {
      final builder1 = MqttClientPayloadBuilder();
      builder1.addString(message);
      _client!.publishMessage(topic, MqttQos.atLeastOnce, builder1.payload!);
    } on Exception catch (e) {
      print('EXAMPLE::client exception - $e');
    }
  }

  void subscribeTopic(String topic) {
    _client!.subscribe(topic, MqttQos.atMostOnce);
  }

  void unSubscribeTopic(String topic) {
    _client!.unsubscribe(topic);
  }

  void disconnect() {
    _client!.disconnect();
  }

  get client => _client;
}

WebRTC와 관련한 코드는 시그널 기능에 따른 많은 코드들이 요구되지만 셈플에서는 가장 기본적인 통신 기능만을 구현 했다.

  _mqtt = MQTTManager(_clientId);
  await _mqtt!.connect(_topic);
  
  ...
  var desc = await _localPc!.createOffer(offerSdpConstraints);
  await _localPc!.setLocalDescription(desc);
  _sendData("offer", desc.sdp);
  
  ...
  void _sendData(event, data) {
    var request = Map();
    request["command"] = "signal";
    request["clientid"] = _clientId;
    request["type"] = event;
    request["data"] = data;
    _mqtt!.publishMessage(_topic, jsonEncode(request).toString());
  }

박병일, 2021.12.5, rtlink.park@gmail.com

iOS/Swift에서 OpenCV 사용

수년전에 iOS에서 OpenCV를 이용해서 작업을 해본적이 있었는데 새로 같은 작은 작업을 하려다보니 다 잊어버려서 OpenCV 프레임워크를 설치하는 과정을 잊어버려서 잊지 않기 위해서, 그리고 새버전의 Swift/Xcode에서 OpenCV와의 연결에 문제가 없는지 확인하기 위해 다시 한번 해보고 아래와 같이 정리 했다.

OpenCV 프레임워크 다운로드

iOS용 OpenCV 프레임워크를 다운 받는다. OpenCV가 버전업 될때마다 친절하게도 iOS용 프레임워크를 빌드해서 다운받을 수 있도록 준비가 되어 있다. 아래 링크에서 프레임워크를 다운 받을 수 있다. 현재 버전은 V4,5,2 최근 버전이 굉장히 빠르게 올라가고 있다. 머신러닝과 관련한 기능들이 많이 추가된것이 원인인듯하다.

https://opencv.org/releases/

다운 받아 압축을 해제 하면 iOS에서 바로 사용할 수 있는 프레임워크 형식의 파일과 폴더로 구성되어 있다.

Xcode 신규 프로젝트 생성

프레임워크 추가

Xcode에서 OpenCV-Test 라는 이름의 신규 프로젝트를 하나 생성 했다. 내가 쓰고 있는 Xcode의 버전은 12.5이다. 위에서 압축 해제한 OpenCV 프레임워크를 폴더를 생성한 프로젝트 폴더로 이동하고 프로젝트를 선택하고 [Build Phase] 에서 Link Binary With Libraries 의 + 버튼을 눌러서 프로젝트에 포함시킨다.

래퍼클래스 생성

File 메뉴에서 New -> File..을 선택해서 아래 그림과 같은 순서로 CVWrapper를 생성한다.

이때 주의할점은 Language 에 Swift 대신에 Objecitive-C 언어로 선택을 해주어야만한다. Next를 선택하면 Xcode는 자동으로 브릿지 Header 파일을 생성하겠냐는 메세지를 보여준다. Create Bridging Header를 선택해서 브릿지헤더파일을 생성한다.

그리고 생성된 CVWrapper.m 파일의 확장자를 CVWrapper.mm 으로 변경해준다.

Prefix Header 파일 생성

그리고 메뉴에서 File – New – File… 을 선택해서 PCH file을 위 그림과 같이 선택해서 주어진 파일명으로 생성하고 생성된 .pch 파일에 아래와 같은 코드를 삽입한다.

//
//  PrefixHeader.pch
//  OpenCV-Test
//
//  Created by Park Billy on 2021/05/07.
//

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#ifdef __cplusplus
#include <opencv2/opencv.hpp>
#endif

#endif /* PrefixHeader_pch */

생성한 Prefix 파일을 컴파일러에 등록해 주어야 하는데 아래의 그림과 같이 Build Settings에서 $(SRCROOT)/프로젝트명/PrefixHeader.pch의 형식으로 등록한다.

OpenCV 버전확인 하기

아래의 코드를 추가해서 OpenCV의 버전을 가져오는 기능을 만들어 봤다. CVWrapper.h / CVWrapper.mm

//
//  CVWrapper.h
//  OpenCV-Test
//
//  Created by Park Billy on 2021/05/07.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CVWrapper : NSObject

+ (NSString *)openCVVersion;

@end

NS_ASSUME_NONNULL_END
//
//  CVWrapper.mm
//  OpenCV-Test
//
//  Created by Park Billy on 2021/05/07.
//

#import "CVWrapper.h"
#import <opencv2/opencv.hpp>

@implementation CVWrapper

+ (NSString *)openCVVersion {
    return [NSString stringWithFormat:@"OpenCV Version %s",  CV_VERSION];
}

@end

ContentView.Swift에서 래퍼클래스의 함수를 호출하여 실제 아이폰 화면에 OpenCV 버전을 보여주는 코드를 아래와 같이 추가했다.

//
//  ContentView.swift
//  OpenCV-Test
//
//  Created by Park Billy on 2021/05/07.
//

import SwiftUI

struct ContentView: View {
    @State var version = ""
    
    var body: some View {
        Text(self.version)
            .padding()
            .onAppear(){
                self.version = CVWrapper.openCVVersion()
            }
    }
}

실행결과

실행 결과는 아래와 같다.

실제로 iOS에서 OpenCV 기능을 활용하려면 위의 CVWrapper.h 에 함수를 정의 하고 CVWrapper.mm에 기능을 구현해서 Swift 코드에서 함수를 호출하는 방식으로 사용하면 된다.

소스코드

아래 링크에서 소스 코드를 다운 받을 수 있다. 이 프로젝트를 테스트 하려면 소스에 OpenCV 프레임워크는 포함되어 있지 않으므로 다운 받아서 이글의 첫번째 부분과 같이 프레임워크를 포함시켜야 한다.

https://github.com/bipark/opencv_ios_starter

20210507 박병일

OpenCV를 이용한 Face Detection & Blur

Face Detection & Blur

OpenCV 공부중에 주말 프로젝트로 만들어본 Go 언어 기반 얼굴인식 기능 및 블러 처리 프로그램입니다.

얼굴인식 – 이미 학습된 Caffe 기반의 딥러닝 얼굴인식 모델을 활용하여 인식하고 인식된 얼굴 부분을 블러 처리하여 웹요청에 응답합니다.

API 서버 – Go Gin 기반으로 웹 프론트에 용청에 응답을 처리

프론트 – Vue.js & Axios 등으로 Go 서버에 이미지를 전송하고 결과를 받아서 화면에 뿌려줍니다.

Open Source

이 프로젝트의 소스코드는 아래 링크를 참조하세요.

https://github.com/bipark/go-opencv-caffe-facedetect-blur

의존성

실행방법

GraphQL + MySQL 데이터 가져오기 스터디

네이버에 개발자 모집 공고를 보다가 React Native 개발자를 구하는데 GraphQL 기반의 백엔드 프로그래밍 역할도 필요하다는 것을 보고 놀랐다. 세상이 진짜 변하긴 했다.

물론 프론트 개발자가 백엔드를 동시에 작업할 수 있으면 매우 생산성이 높고 문서작업 & 협의 과정 없이 빠르게 서비스를 만들수 있다. 나는 Node.js 와 Vue.js를 이용해서 그런방식으로 작업을 하고 있는데 매번 프론트에서 엔드포인트가 필요할때마다 Node에서 api를 만들고 그걸 받아서 쓰고 있지만 프론트에서 이걸 끝내는 방법이 없나 하는 생각은 늘 하고 있던 차였다.

GraphQL이 이걸 해줄 수 있다고 해서 공부를 해봐야지 한게 벌써 두해를 넘기고 있기도 하고 여러가지 문서를 읽어 봤지만 역시 만들어 보는게 최고라… 그게 뭔지는 대충 아래 링크로 퉁치고 Hello World 와 좀 더 실제적인 Mysql DB에서 데이터를 읽어와서 뿌려주는데까지 한번 해보기로 했다.

Node.js & Express.js 로 서버를 구성하면 빠른방법으로 Hello World를 찍어 볼 수 있다.

> express --view=pug
> npm install express-graphql graphql

Express Generator를 이용해 프로젝트를 생성하고 npm으로 express-graphql, graphql을 설치했다.

var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');

var app = express();

var schema = buildSchema(`
  type Query {
    hello: String
  }  
`);

var root = {
  hello: ()=>{
    return 'Hello World'
  },
};

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

코드를 실행하고 http://localhost:3000/graphql 에 접속하면 다음과 같은 화면을 볼 수 있다. 이것은 app.use() 에서 graphiql 옵션을 true로 셋팅하면 나타나는 화면으로 GraphQL의 테스트 인터페이스이다. 여기서 api를 미리 테스트 해볼 수 있다. 자동완성 기능도 지원한다.

Hello World는 찍었다. 대충 이제 다 알았다…는 아니고 현재 가장 많이 쓰고 있는 DB인 MySQL에 연결해서 데이터를 가져오는데 까지 해야한다. Node에서 MySQL 연결과 기타 과정은 생략하고 아래와 같은 코드를 작성 했다.

var app = express();
var schema = buildSchema(`
  type Query {
    user: [User]
  }
  
  type User {
    name: String
    age: Int
    sex: String
  }  
`);

var root = {
  user: ()=>{
    return new Promise(function (resolve) {
      pool.query('select * from users', function (err, res) {
        resolve(res);
      });
    });
  },
};

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

의의 코드를 실행한 결과로 아래와 같은 화면을 얻었다.

분명 데이터는 나왔는데 뭔가 잘못한게 아닌가 싶은 생각이 들었다. 타입도 서버에서 선언해 줘야 하고 쿼리도 서버에서 실행시킨다면 이게 기존의 REST API와 무었이 다른가? 그저 한개의 엔드 포인트를 쓴거 말고 다른게 있나 싶은 생각이 들었다.

그래서 좀 더 검색을 해봤다. 아래 링크에서 하나의 엔드포인트가 중요한 차이점이며 Query의 요청과정에서 파라메타의 차이로 결과를 다르게 가져올 수 있다는 점이 중요하다고 알려줬다.

https://www.holaxprogramming.com/2018/01/20/graphql-vs-restful-api/

결론

나는 이걸 어떻게 써야 하나 하는 문제가 고민이었는데 어차피 현재 Node.js 를 서버로 쓰고 있기 때문에 기존의 REST API에 GraphQL을 더 붙여 써도 괜찮지 않을까 하는 생각을 했다.

개발자들이 뭔가 바꾸고 싶으면 아주 갈아엎는(?) 나쁜 습성이 있지만 그러지 않아도 된다면 엔드 포인트 한두개씩 GraphQL로 바꿔도 무방할지 않을까 싶다.

소스코드는 아래 링크에 있습니다.

https://github.com/bipark/graphql_study