엔지니어링 노트음성과 회의

녹음이 끊긴 회의는 끝내지 않는다, 같은 시간축에 이어 녹음한다

회의 녹음은 브라우저 탭 하나에서 한 시간 넘게 돕니다. 그 사이 와이파이가 끊기고, 노트북이 절전에 들어가고, 누군가 탭을 닫거나 새로 고치고, 로그인이 만료되고, 발표자가 노트북을 덮은 뒤 휴대폰으로 이어 가려 합니다. 회의가 끝나면 원본 음성을 지우므로(「회의를 끝내면 마지막 발언이 빠진다, 음성은 받아쓰기를 마친 뒤 지운다」), 끊긴 동안의 소리를 서버에 올리지 못했거나 끊김을 회의 종료로 읽으면 그 구간은 되살릴 수 없습니다. 두 창이 같은 회의를 함께 올리면 두 기기의 소리가 한 타임라인에 섞입니다.

이 글은 이 조건에서 음성을 지키는 설계를 다룹니다. 녹음 조각은 브라우저에 먼저 쌓고, 녹음하는 창은 리스(lease) 하나로 정하고, 끊긴 회의는 끝내지 않고 멈춰 두었다가 같은 시간축에 이어 붙이고, 절전 구간은 기록에 남깁니다.

서버는 끊긴 이유를 거의 알 수 없다

녹음이 끊기는 원인은 여럿인데, 서버가 받는 신호는 대개 같습니다. 조각이 더 오지 않는다는 것뿐입니다.

원인 서버가 보는 것
네트워크 끊김(녹음은 계속됨) 조각과 폴링이 끊긴다
탭 닫기·새로 고침 떠나기 직전의 닫힘 요청(못 올 때도 있다)
강제 종료·정전·브라우저 크래시 조각과 폴링이 끊긴다
절전·화면 잠금 몇 분 조용하다가 조각이 다시 온다
로그인 만료 업로드가 401로 거절된다
다른 기기에서 이어받기 같은 회의에 녹음이 하나 더 열린다

신호가 끊긴 회의 가운데 정말 끝난 것을 서버는 가릴 수 없습니다. 로그인이 만료된 사람은 그 뒤로도 말을 이어 가고, 절전에서 깨어난 노트북은 몇 분 뒤 다시 조각을 보냅니다. 가장 단순한 구성은 녹음 조각을 메모리에 들고 있다가 바로 올리고, 일정 시간 신호가 없으면 회의를 끝내는 것입니다. 그러면 전송 중에 탭이 닫힐 때 메모리의 조각이 사라지고, 끝낸 회의는 음성을 지우는 종착 상태가 되어 다시 로그인하고 돌아온 사람이 이어 녹음할 길이 닫힙니다. 그래서 회의를 끝내는 것은 사람이 회의 종료를 눌렀을 때와, 끊긴 채 보존 기간을 넘겼을 때뿐입니다.

녹음 조각은 브라우저 저장소에 먼저 넣고, 서버가 받았다고 답하면 지운다

브라우저는 녹음을 5초마다 조각으로 잘라 냅니다(「회의가 겹치면 받아쓰기가 멈춘다, 큐는 쓰는 자원마다 나눈다」). 조각은 먼저 브라우저의 IndexedDB에 넣고, 그다음 서버로 보내고, 서버가 받았다고 답한 뒤에 지웁니다. 순서를 바꿔 보낸 뒤에 넣으면, 전송 중에 탭이 닫혔을 때 그 조각은 어디에도 없습니다. 탭을 새로 고치면 다음 로드에서 남은 조각부터 올립니다. 서버는 받은 조각을 즉시 객체 저장소에 씁니다(「재배포 뒤에도 남아야 할 파일은 객체 저장소에만 둔다」).

이 저장소에는 아직 못 올린 조각만 둡니다. 네트워크가 5분 끊겨도 10MB 안쪽이라, 회의 전체(4시간에 460MB)를 쌓을 때와 달리 브라우저가 저장 공간이 모자라 사이트 데이터를 지우는 범위에 들지 않습니다.

서버가 조각을 도착 순서대로 이어 붙이므로 조각은 녹음 순서대로 하나씩 보냅니다. 실패하면 종류에 따라 다르게 움직입니다.

  • 네트워크 오류나 5xx면 조각을 버리지 않는다. 연결이 돌아올 때까지 그 조각에서 기다렸다가 앞의 조각부터 다시 보낸다.
  • 401이면 조각을 그대로 두고 녹음을 멈춘다. 다시 로그인하면 남은 조각이 이어서 올라간다.
  • 회의가 이미 끝났거나, 다른 기기가 녹음을 이어받았거나, 녹음 길이 상한(기본 4시간)에 닿았다는 409면 녹음을 멈춘다.
  • 형식·크기 거절처럼 다시 보내도 같은 답이 오는 조각만 건너뛰고 화면에 알린다. 그 조각이 맨 앞을 막으면 뒤의 녹음이 하나도 올라가지 못한다.

401에서 녹음을 계속하면 사용자는 기록되는 줄 알고 말하는데 서버는 한 조각도 받지 못합니다. 한 건도 올리지 못한 채 60초가 지나면 화면은 "서버에 저장되지 않고 있습니다 — 연결을 확인하세요. 녹음은 계속되고 있습니다"를 띄웁니다.

녹음하는 창은 30초 리스 하나로 정한다

한 창이 마이크를 잡고 녹음하는 한 구간을 캡처 세션이라고 부르고, 세션을 열 때 그 회의의 녹음 리스를 함께 잡습니다. 리스는 닫히지 않았고 마지막 신호가 30초(meeting.captureLeaseTimeoutSeconds) 안에 온 세션의 것입니다. 쿠버네티스가 노드 하트비트에 리스를 쓰는 것과 같은 방식입니다.

리스는 조각 업로드와 회의 화면의 폴링(기본 1.5초)이 둘 다 연장합니다. 폴링도 연장하는 이유는 마이크 입력이 잠깐 끊겨도 사람은 화면 앞에 있기 때문입니다. 거꾸로 뒤에 깔린 탭에서는 브라우저가 폴링 타이머를 늦추지만, 녹음기의 조각은 5초마다 옵니다. 둘 중 하나만 살아 있어도 리스는 만료되지 않습니다.

리스가 살아 있는 동안 다른 창이 같은 회의의 녹음을 열려고 하면 서버는 409로 거절하고, 화면은 "이 회의를 다른 창 또는 기기에서 녹음 중입니다."와 여기서 이어받기 버튼을 보입니다. 이어받기는 리스가 만료되기를 기다리지 않고 바로 넘겨받습니다. 노트북이 꺼져 휴대폰으로 옮기는 사람을 30초 기다리게 하지 않습니다. 리스를 잃은 창은 다음 업로드나 폴링에서 409를 받고 녹음을 멈추며 "다른 창 또는 기기에서 녹음을 이어받았습니다."를 띄웁니다. 리스를 잃은 창이 재시도로 같은 세션을 되살리면 두 기기의 오디오가 한 타임라인에 섞이므로, 다시 녹음하는 창은 언제나 새 세션을 엽니다.

같은 브라우저의 두 탭은 마이크를 잡기 전에 Web Locks API로 걸러 냅니다. 서버 리스만 있으면 두 번째 탭은 녹음을 시작한 뒤에야 409를 받고, 사용자는 그 사이 녹음되는 줄 알고 말합니다. 잠금은 기다리지 않고 한 번만 시도합니다. 기다리게 두면 잊고 있던 탭이 앞 탭이 끝나는 순간 마이크를 켭니다.

끊긴 회의는 10분 뒤 일시정지로 남기고, 7일 뒤에 끝낸다

신호가 30초 끊기면 리스가 만료되어 다른 창이 이어받기 없이도 녹음을 열 수 있습니다. 세션은 아직 닫지 않습니다. 네트워크가 돌아온 창은 같은 세션으로 조각과 폴링을 다시 보내고, 사용자가 아무것도 누르지 않아도 녹음이 이어집니다.

마지막 신호 뒤 30초·10분·7일에 바뀌는 녹음 리스와 회의 상태
그림 1. 마지막 신호 뒤 30초·10분·7일에 바뀌는 녹음 리스와 회의 상태

주기 작업이 5분마다 10분 넘게 신호가 없는 캡처 세션을 찾아 닫습니다. 회의는 녹음 상태 그대로 일시정지로 남고, 목록 카드에는 중단됨이 붙습니다. 강제 종료·정전·크래시는 신호를 남기지 않아, 이 작업이 없으면 세션이 열린 채 남습니다. 돌아온 사람은 회의를 열어 녹음 이어하기를 누르거나 다른 기기에서 이어 갑니다. 탭을 닫거나 새로 고칠 때는 떠나기 직전 요청으로 세션을 바로 닫으므로 10분을 기다리지 않고 바로 일시정지가 됩니다.

10분은 돌아온 창이 사용자의 조작 없이 계속 녹음할 수 있는 시간입니다. 세션이 닫힌 뒤 돌아온 창은 폴링에서 409를 받고 녹음을 멈춥니다. 그래도 그 창의 브라우저에 쌓여 있던 조각은 받습니다. 이미 녹음된 과거이고 회의 안의 위치가 정해져 있어서, 받아도 잃을 것이 없고 버리면 되살릴 수 없습니다. 다른 기기가 리스를 쥐고 있을 때만 그 기기의 타임라인에 섞이지 않게 거절합니다. 조각을 받아도 리스는 되살리지 않고, 계속 녹음할지는 사용자가 녹음 이어하기로 정합니다.

일시정지가 7일(PAUSED_MEETING_RETENTION_DAYS)을 넘기면 회의를 끝냅니다. 끝내는 경로는 사람이 누른 종료와 같아서, 남은 구간을 받아쓰고 회의록을 만든 뒤 음성을 지웁니다(「회의를 끝내면 마지막 발언이 빠진다, 음성은 받아쓰기를 마친 뒤 지운다」). 끝나지 않은 회의는 원본 음성을 기한 없이 들고 있으므로, 이 상한이 「회의가 끝나면 원본 음성은 보관하지 않는다」는 약속을 일시정지된 회의에도 지킵니다.

이어 녹음한 소리는 앞 세션들이 녹음한 길이 뒤에 붙는다

브라우저는 자기 캡처 세션의 시작을 0으로 잡은 상대 시각만 보냅니다. 회의 안의 위치는 서버가 세션을 열 때 한 번 정하는데, 새 세션의 시작 위치는 그 회의의 앞 세션들이 녹음한 길이의 합입니다. 새로 고침으로 브라우저의 0이 다시 시작돼도 타임라인이 겹치지 않고, 조각마다 다른 업로드 지연이 위치에 섞이지 않습니다.

세션과 세션 사이에 쉰 시간은 합에 들어가지 않습니다. 쉰 자리는 받아쓰기에 "일시정지 · {duration}" 한 줄로 남습니다. 새로 고침이나 이어받기로 세션이 바뀌는 순간까지 줄이 생기지 않게, 3초보다 짧은 틈은 줄로 남기지 않습니다. 오디오 트랙도 세션마다 따로 저장해, 한 세션의 소리가 다른 세션의 트랙에 섞이지 않습니다.

절전과 일시정지를 거쳐 세 캡처 세션이 한 회의 시간축에 이어지는 모습
그림 2. 절전과 일시정지를 거쳐 세 캡처 세션이 한 회의 시간축에 이어지는 모습

절전 구간은 비워 두지 않고 「녹음되지 않은 부분」으로 남긴다

녹음하는 동안 데스크톱 앱은 운영체제의 절전 차단을 걸고, 브라우저는 Screen Wake Lock API로 화면 꺼짐을 막습니다. 그래도 노트북 덮개를 닫거나, 탭이 가려져 브라우저가 잠금을 놓은 사이에 기기는 잠듭니다.

기기가 멈췄는지는 조각이 도착한 간격으로 판정합니다. 조각은 평소 5초마다 오고 뒤에 깔린 탭에서도 같습니다. 30초 넘게 조각이 오지 않았다가 다시 오면 그동안 기기가 멈춰 있었다고 봅니다. 타이머 간격으로 재면 뒤에 깔린 탭의 늦춰진 타이머를 절전으로 오판합니다.

깨어나면 브라우저는 캡처 세션을 새로 열어 갈아탑니다. 새 녹음기를 먼저 켜서 세션을 바꾸는 동안의 소리도 남기고, 그 조각은 새 세션이 열릴 때까지 브라우저 저장소에서 기다립니다. 옛 세션은 남은 조각을 다 올린 뒤 닫습니다.

세션을 갈아타는 이유는 녹음 길이를 실제 흐른 시간으로 재기 때문입니다. 잠든 동안에도 길이는 자라므로, 같은 세션으로 계속 올리면 잠든 구간은 소리가 없는데도 서버에는 아직 오지 않은 녹음으로 보입니다. 서버는 그 구간을 받아쓰려고 몇 분씩 다시 시도하고, 회의를 끝낼 때도 그만큼 기다립니다. 새 세션이 리스를 쥐면 서버는 옛 세션의 그 구간에 더 올 소리가 없다고 보고 곧바로 「녹음되지 않음」으로 확정합니다. 시간축은 실제 시각 그대로라 깨어난 뒤의 발언은 실제 시각에 놓입니다.

화면에는 "기기가 약 {count}분 동안 절전·화면 잠금 상태여서 그동안은 녹음되지 않았습니다"가 뜨고, 회의록에는 그 구간이 "녹음되지 않은 부분"으로 남습니다. 비워 둔 채 넘어가면 회의록을 읽는 사람은 그 몇 분 동안 아무 결정도 없었다고 읽습니다. 말이 없던 구간은 빠진 것이 아니므로 따로 셉니다.

설계에서 고른 것

끊긴 회의를 끝내는 대신 일시정지로 남깁니다. 끝내면 상태가 단순하고 회의록도 바로 나옵니다. 하지만 서버는 끊긴 회의와 끝난 회의를 가릴 수 없고, 잘못 끝내면 이어 녹음할 길이 영영 닫힙니다. 대가는 일시정지된 회의가 원본 음성을 들고 있다는 점이고, 그래서 7일 보존 상한을 둡니다. 진행 중인 회의는 한 사람당 하나라서, 일시정지된 회의를 둔 채 새 회의를 시작하면 "일시정지된 회의가 있습니다" 창이 재개와 끝내고 새로 시작을 묻습니다.

조각을 메모리 대신 브라우저 저장소에 먼저 둡니다. 메모리는 단순하지만 탭이 닫히면 조각도 사라집니다. 사이트 데이터를 막은 브라우저처럼 IndexedDB를 쓸 수 없으면 메모리로 이어 가고, 그때는 탭이 살아 있는 동안만 지킵니다.

닫힌 세션을 되살리는 대신 언제나 새 세션을 엽니다. 대가는 리스를 잃은 창이 다시 녹음하려면 버튼을 한 번 더 눌러야 한다는 점이고, 그 덕에 한 타임라인에는 한 기기의 소리만 들어갑니다.

절전 뒤에는 같은 세션을 이어 쓰는 대신 세션을 갈아탑니다. 이어 쓰는 편이 구현은 짧지만, 잠든 구간이 서버의 재시도와 회의 종료를 몇 분씩 붙잡습니다.

브라우저 탭에서 오래 도는 녹음이나 업로드를 만든다면, 서버가 받는 신호만으로 끊긴 이유를 가를 수 있는지부터 보면 됩니다. 가를 수 없다면 끊긴 작업은 끝내지 말고 멈춰 두고, 끝내는 결정은 사람과 보존 기간에 맡기는 편이 되돌릴 수 없는 손실을 막습니다.

어떤 문제를 해결하나요?

음성과 회의의 다른 글