reMarkable Streaming Tool v2: 원격 근무 효율성 향상
(blog.owulveryck.info)- 화상회의 중 reMarkable 2 스케치를 공유하던 도구가 노트북 로컬 서비스 없이 열리도록 바뀌어, 발표자가 브라우저만으로 즉석 스트리밍을 시작할 수 있게 됨
- 새 구조는 reMarkable 내부 HTTP 서버와 브라우저의 JavaScript 클라이언트가 원시 이미지를 받아
canvas에 그리는 방식으로 단순화됨 - WebSocket 대체안은 동작했지만 iOS 문제와 서버 오버헤드가 남아, 고정 해상도 이미지를
http.ResponseWriter로 계속 쓰고fetch스트림으로 읽는 원시 스트림 방식으로 정리됨 - 1872x1404 원시 프레임은 약 2.5MB라서, 펌웨어 3.3 이후 16색 값을 uint4로 패킹해 50% 줄이고 RLE로 평균 약 200KB 전송량까지 낮춤
/dev/input/event*감시로 입력이 없을 때는 새 프레임 전송을 멈추며, 연결된 클라이언트가 있어도 CPU 사용량은 0으로 떨어지고 필기 중에는 약 10% CPU로 동작함
기존 도구가 불편했던 이유
- 2021년에 만든 reMarkable 스트리밍 도구는 화상회의 중 스케치를 공유하는 데 쓰였고, 브라우저 탭만 공유하면 되어 발표 내용에 집중하기 쉬웠음
- 기존 구현은 세 구성 요소로 나뉨
- 서버: reMarkable 장치에서 실행되며 현재 화면의 원시 이미지를 노출함
- 클라이언트: 노트북에서 서버의 원시 이미지를 가져와 브라우저가 볼 수 있는 형식으로 처리함
- 렌더러: 브라우저나 VLC처럼 HTTP MJPEG 스트림을 읽어 화면에 표시함
- 장치 CPU 사용을 줄이기 위해 서버는 클라이언트가 연결됐을 때만 이미지를 추출했고, 통신에는 gRPC가 사용됨
- 노트북 클라이언트는 이미지를 반복해서 가져오고 JPEG로 인코딩한 뒤 MJPEG 스트림을 HTTP 서비스로 제공함
- 발표 환경에서는 reMarkable 주소, 클라이언트 실행 권한, 렌더러가 알아야 하는 클라이언트 IP 같은 네트워크 구성이 부담이 됨
브라우저만으로 여는 새 구조
- 새 목표는 어떤 브라우저에서든 reMarkable 주소만 입력하면 스트림에 접근하게 만드는 것임
- 별도 노트북 클라이언트를 없애고, reMarkable의 서버 컴포넌트 안에 HTTP 서버를 넣는 구조로 바뀜
- 브라우저에서 실행할 클라이언트는 JavaScript 또는 WASM 형태가 필요했음
- 처음에는 Go 경험을 활용하기 위해 WASM 컴파일을 검토했지만, 상당한 수정이 필요한 제한 때문에 포기함
- 두 번째 버전의 클라이언트는 최종적으로 JavaScript로 작성됨
- JavaScript 코드 조각과 설명을 얻는 과정에서 ChatGPT를 사용했지만, 원하는 해법의 방향은 직접 정함
canvas 렌더링 방식
- MJPEG 스트림에서 벗어나기 위해 브라우저의 이미지 조작 기본 요소인
canvas를 사용함 - reMarkable에서 받은 원시 이미지를
Uint8Array로 읽고,ImageData의 RGBA 픽셀 데이터에 같은 값을 R/G/B로 넣은 뒤 알파 값을 255로 설정해 표시함 - 반응형 표시, 회전, 색상화 가능성을 위해 고정 크기의
fixedCanvas를 숨긴 상태로 유지함 - 표시용 캔버스에는
drawImage로 숨겨진 캔버스 내용을 복사함 - 브라우저 창 크기가 바뀌면 컨테이너 크기와 1872/1404 비율을 기준으로 표시 캔버스의 폭과 높이를 조정함
WebSocket을 버리고 원시 스트림으로 전환
- gRPC는 웹 개발에서 일반적인 선택이 아니어서, 첫 번째 대체 구현은 WebSocket을 통신과 캡슐화 수단으로 사용함
- WebSocket 메시지는 원시 이미지를 담고, 브라우저 클라이언트는 메시지를 받을 때마다 캔버스를 업데이트해 스트리밍처럼 보이게 함
- 이 방식은 서버 측 메시지 전송 빈도를 조절해 reMarkable의 메모리와 CPU 부하를 관리할 수 있었음
- 하지만 iOS에서 문제가 있었고, 서버 측 WebSocket 구현의 오버헤드도 제어하기 어려웠음
- 최종 구조는 캡슐화를 없애고, 고정된 이미지 크기를 이용해 원시 이미지를 네트워크로 직접 전송함
- Go 서버는
http.ResponseWriter에 이미지를 반복해서Write함 - 브라우저 클라이언트는
fetch('/stream')의ReadableStream을 읽고, 들어온 청크를 캔버스 데이터로 반영함
- Go 서버는
전송량 최적화
- reMarkable 2의 원시 이미지는 해상도 1872x1404 기준 약 2.5MB이며, 이 데이터가 프레임마다 전송돼야 함
- 펌웨어 3.3 이후 reMarkable의 16가지 색상 값은 uint8 대신 uint4 배열로 표현할 수 있음
- Go와 JavaScript에는 네이티브 uint4 타입이 없음
- 두 픽셀 값을 하나의 uint8 바이트에 저장하는 방식으로 우회함
- Go에서는 두 uint4 값을 상위 4비트와 하위 4비트로 패킹하고, JavaScript에서는 이를 다시 언패킹함
- 이 표현은 데이터량을 50% 줄일 수 있음
- 추가 압축에는 Run Length Encoding (RLE)을 사용함
- RLE는 같은 픽셀 값이 연속되는 횟수와 값을 함께 전송하는 단순한 알고리듬임
- 예시
0 0 0 0 0 0 1 1 1 0 0 0 0은6 0 3 1 4 0으로 표현됨
- 카운트 값은 최대 1872*1404까지 커질 수 있어 uint64 같은 타입이 필요할 수 있고, 경우에 따라 압축 결과가 원본보다 커질 위험이 있음
- 이를 피하기 위해 카운트 길이를 15로 제한하고, 카운트와 픽셀 값을 한 바이트에 담는 균형점을 택함
- RLE 구현은 Go의
io.Writer처럼 동작해 재사용 가능하며, 필요한 경우 RLE를 두 번 적용할 수도 있지만 현재는 필요하지 않았음 - 패킹과 RLE 이후 평균 전송량은 약 200KB 수준임
변경이 있을 때만 프레임 전송
- 마지막 최적화는 화면이 바뀔 때만 새 프레임을 보내는 방식임
- 변경 여부를 체크섬으로 계산하면 CPU 부담이 커질 수 있음
- reMarkable은 Linux 기반이라 펜이나 터치 입력이
/dev/input/event*로 전달됨 - goroutine이 이 입력 이벤트를 감시하고, 필요한 경우에만 이미지를 전송함
- 이벤트가 없으면 클라이언트가 연결된 상태에서도 CPU 사용량은 0으로 떨어짐
- 필기 중 CPU 사용량은 약 10% 수준임
펌웨어 변경과 유지보수 부담
- 이 애플리케이션은 해킹에 기반하며, 핵심 과제는 이미지를 가져오는 인터페이스와 클라이언트/렌더러를 효과적으로 분리하는 것임
- 이전 구현은 protobuf 정의를 통해 클라이언트와 서버가 완전히 분리돼 있었음
- reMarkable 3.3 펌웨어는 도구를 깨뜨렸고, 관련 내용은 GitHub issue 36에 있음
- 당시 수정은 클라이언트 컴포넌트에만 영향을 줌
- 펌웨어 3.6도 GitHub issue 58에 따르면 깨지는 변경을 가져올 수 있음
- 이 경우 더 넓은 수정이 필요할 수 있음
- 다만 클라이언트가 서버에 통합된 자체 포함 구조라 장치 업데이트는 더 단순해질 수 있음
- 애플리케이션과 소스 코드는 github.com/owulveryck/goMarkableStream에서 제공됨
댓글과 토론
Hacker News 의견들
-
reMarkable 태블릿 화면을 노트북으로 스트리밍하던 2021년 도구를 다시 다듬어 공개했고, 새 글에서는 아키텍처, 구성 요소, 사용자 경험을 개선한 과정을 깊게 다뤘음
제품 관리자 관점에서 사용자가 어떻게 느끼는지 보면서 활성화 과정을 단순화했고, 로컬 서비스 없이 동작하게 만들고 네트워크 사용량도 최적화함- 멋진 프로젝트처럼 보임.
Ctrl-C후./goMarkableStream으로 재시작하니 어느 정도 동작했지만, 여전히waiting for reMarkable screen이 자주 뜨며 서비스가 불안정함
reMarkable2를 3.5.2.1807로 올린 뒤 설치했는데, 노트북·시트·책 위에 그려도 반응이 없었고read /dev/input/event2: file already closed,read /dev/input/event1: file already closed같은 로그가 가끔 보임
https://192.168.8.143:2001/와 https://10.11.99.1:2001/ 모두 HTML과 캔버스를 제공하고, Chrome·Firefox·Brave에서도 시도했음
스트림당 브라우저 하나, IP 하나 제한처럼 보이지만, 어떤 주소와 브라우저를 써도waiting for reMarkable screen이 뜰 때가 있음.nohup ./goMarkableStream &후 PuTTY를 닫고 클라이언트를 재시작하자 모든 브라우저가 같은 상태가 됐고, https://10.11.99.1:2001/stream을 보면too many requests가 반환됨. 스트림을 어떻게 재시작해야 할지 궁금함 - USB 연결로도 구현할 수 있을지 궁금함
- 멋진 프로젝트처럼 보임.
-
대안으로 SuperNote를 매우 만족하며 쓰고 있음. 화면 미러링이 가능해서 회의 중 다이어그램을 빠르게 그리기에 아주 좋음
단점은 SuperNote가 작은 웹 서버를 띄우고 Firefox로 접속하는 방식이라, 노트북과 SuperNote가 같은 네트워크에 있어야 한다는 것임. 홈오피스에서는 문제가 없지만 회사 정책상 막힐 수 있음
RM2든 SuperNote든 펜과 종이로 아이디어를 쓰는 걸 좋아하는 사람에게는 훌륭한 도구이고, 앱이나 텍스트 문서와는 느낌이 꽤 다르며 노트에 낙서도 할 수 있음
[0]: https://supernote.com/- Onyx Boox Note도 잘 동작하고, 5년 넘게 지난 뒤에도 업데이트를 내고 있음
다만 구매할 때는 GPL 위반을 감수해야 함. 완전히 Android 기반인데도 운영체제 소스를 공개하지 않음 - 전자책 리더 겸 노트 필기용 전자잉크 태블릿을 찾고 있었는데 추천이 도움이 됨. Remarkable 2와 Boox 사이에서 고민 중이었고, SuperNote의 소프트웨어 업데이트 경험이 어떤지 궁금함
앞으로 3~5년 동안 기능 업데이트나 최소한 보안 업데이트라도 지원받지 못할 기기를 사게 될까 봐 걱정됨 - 기억으로는 SuperNote가 함께 배포하는 소프트웨어의 GPL을 준수하지 않았는데, 입장이 바뀌었는지 궁금함
- 아직 “같은 네트워크에 있어야 하는” 문제는 겪지 않았지만, 네이티브 Ngrok 기능을 넣는 건 몇 분이면 되는 간단한 작업으로 보임. 그러면 인터넷을 통해 스트리밍할 수 있음
- SuperNote의 필기감이 RM2와 비교해 어떤지 궁금함
- Onyx Boox Note도 잘 동작하고, 5년 넘게 지난 뒤에도 업데이트를 내고 있음
-
HTML 캔버스 렌더링은 여기서 설명한 것처럼 형식화 배열을 쓰면 더 빨라질 수 있음: https://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipu...
- 고마움, 한번 살펴보겠음
-
이런 글이야말로 여기서 보고 싶은 콘텐츠임. ChatGPT가 잘 모르는 영역의 문제를 배우고 해결하는 데 어떻게 도움을 줬는지가 좋았고, “내가 개발자이고 ChatGPT가 코더였다”는 표현에 공감함
단순함은 실제로 복잡하다는 말도 맞음 -
JPEG를 고른 건 MJPEG로 바꾸기 쉽고, 지원하는 쪽에 넘기면 디코딩을 공짜처럼 처리할 수 있어서였을 것 같음. 다만 그게 reMarkable CPU에 큰 부담을 주는 요인일 수 있음
JPEG는 사진에 더 적합하지만 reMarkable 화면은 일러스트에 가깝고, 게다가 흑백 계열임. PNG 같은 다른 일반 이미지 형식이나 단순한 RLE 압축만 써도 CPU 부담이 더 낮을 것 같음- 엄밀히 말하면 reMarkable은 단색이 아니라 회색조이고, 기억상 16단계 회색을 지원함. 동반 앱에서는 펜은 파랑·빨강, 형광펜은 노랑·초록처럼 보이는 컬러 잉크도 있음
그리고 독자 파일 형식은 비트맵이 아니라 필기 입력 기반임 - 처음에 JPEG를 고른 이유는 맞음. 다만 그래서 클라이언트/서버 구조를 택했고, 인코딩은 태블릿이 아니라 클라이언트인 노트북에서 수행했음
프로파일링해 보니 CPU 대부분은 선로를 통한 데이터 전송에 쓰였고, 그래서 압축을 넣었음. 지금은 CPU 사용량이 낮음
- 엄밀히 말하면 reMarkable은 단색이 아니라 회색조이고, 기억상 16단계 회색을 지원함. 동반 앱에서는 펜은 파랑·빨강, 형광펜은 노랑·초록처럼 보이는 컬러 잉크도 있음
-
프레임버퍼의 변경된 영역만 전송하는 방식은 검토해 봤는지 궁금함. 그러면 데이터 전송률을 크게 줄일 수 있음: https://github.com/pl-semiotics/mxc_epdc_fb_damage
rM VNC 프로젝트도 그렇게 하지만, 클라이언트 쪽 소프트웨어가 필요 없는 이 앱의 사용자 경험이 더 마음에 듦- 그 방식은 기기에서 어느 정도 분석이 필요하다는 게 문제이고, 코드는 최대한 덜 침습적으로 두고 싶음. 저렴하게 처리할 방법이 있는지 살펴보겠음
-
정말 멋지고 ReMarkable 2를 좋아하고 싶지만, 안전하지 않은 기기라는 입장 때문에 쉽지 않음: https://support.remarkable.com/s/article/Does-reMarkable-off...
- 링크 내용은 이 기기가 대체하려는 종이와 같은 수준의 물리적 보안만 갖는다는 뜻임. 즉 누군가 기기에 접근하면 읽을 수 있음
네트워크에 연결된 불안전한 기기라고 할 때 흔히 떠올리는, 알려진 소프트웨어 취약점 얘기는 아님 - 비공식적으로 gocryptfs 기반 홈 디렉터리 암호화가 가능함: https://github.com/RedTeamPentesting/remarkable-encryption
- 소프트웨어도 현재로서는 매우 제한적임. 공식적으로 마켓플레이스나 확장 기능을 기기에서 쓸 수 있게 해주지 않는 점이 아쉬움
- 전체 디스크 암호화를 제공하는 전자책 리더가 있나?
- 링크 내용은 이 기기가 대체하려는 종이와 같은 수준의 물리적 보안만 갖는다는 뜻임. 즉 누군가 기기에 접근하면 읽을 수 있음
-
“처음에는 클라이언트를 WASM으로 컴파일하려 했다. Go 개발 경험을 활용할 수 있어 유망해 보였지만, 상당한 수정이 필요한 여러 한계에 부딪혔다”는 부분을 더 읽어보고 싶음
- 핵심 문제는 gRPC 라이브러리였고, 현재 지원이 매우 제한적이었음. 또 Go에서 JPEG 압축은 느리고 CPU를 많이 씀
설령 MJPEG 스트림을 생성하더라도 어떻게 표시할지도 문제였음. 캔버스 방식을 생각했지만, WASM과 JS 사이에서 큰 복사를 하지 않고는 캔버스 백엔드에 접근하기 어려웠고, 크기도 2.5MB였음
결국 WASM에 의존하면 이미지 회전처럼 JS에서 기본으로 접근 가능한 이미지 기본 연산을 많이 직접 구현해야 할 것 같았음
- 핵심 문제는 gRPC 라이브러리였고, 현재 지원이 매우 제한적이었음. 또 Go에서 JPEG 압축은 느리고 CPU를 많이 씀
-
이 도구가 내장 스트리밍, 즉 화면 공유 기능과 어떻게 다른지 궁금함
- 내장 기능을 쓰려면 데스크톱 앱을 설치해야 하고, 알기로는 Linux 버전이 없음
https://support.remarkable.com/s/article/Screen-Share
글의 해결책은 충분한 기능을 갖춘 브라우저만 있으면 Linux에서도 동작하는 것으로 보임 - 가장 큰 차이는 이제 클라이언트 설치가 필요 없다는 점임. 브라우저에 reMarkable 주소만 입력하면 내용을 볼 수 있음
- 이미 이 기능이 된다고 생각했음. 화면 공유는 충분히 괜찮고, 라이브 스트리밍에도 쓰고 있음
- 내장 기능을 쓰려면 데스크톱 앱을 설치해야 하고, 알기로는 Linux 버전이 없음
-
reMarkable은 좋아하지만, 앞으로도 돈 낼 생각이 없는 구독보다 이런 스트리밍 기능에 집중해 줬으면 함