-
Notifications
You must be signed in to change notification settings - Fork 0
Feature(#180): 발표자의 영상과 음성 스트림을 파일로 저장 #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature(#180): 발표자의 영상과 음성 스트림을 파일로 저장 #181
Conversation
발표자의 음성 또는 영상을 프레임 단위로 받고, 해당 데이터를 버퍼에 저장한다. 이후 강의가 종료되면 버퍼에 저장되어 있던 데이터를 ffmpeg 모듈을 이용하여 mp4 파일로 추출한다.
tracks.getTracks().forEach((track) => { | ||
if (track.kind === 'video') { | ||
videoSink = new RTCVideoSink(track); | ||
} | ||
if (track.kind === 'audio') { | ||
audioSink = new RTCAudioSink(track); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
발표자 track이 추가될 때 각 track을 받아서 sink 객체를 만들어주는거 같은데, sink 객체가 어떤 걸 하는걸까요? 지금 찾아봤을 때는 frame 이벤트를 발생시켜준다고 하는데 어떤 역할을 하는 객체인지 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아마 이 도큐먼트를 보셨을텐데, 해당 도큐먼트에 나와있는대로 추가한 트랙에서 프레임(또는 샘플) 단위로 데이터를 받을 수 있도록 구현해놓은 node-WebRTC의 API입니다 (미디어 데이터에서 특정 크기의 데이터를 복사해서 가져온 후 frame(또는 data) 이벤트를 발생시키는 것 같습니다)
대강 어떻게 흘러가는지 예측해볼 순 있지만, 정확한 정보는 구현체를 보는 방법밖에 없어서 더 궁금하시다면 해당 코드를 참고해보시면 될 것 같습니다.
https://github.com/node-webrtc/node-webrtc/blob/develop/src/interfaces/rtc_audio_sink.cc
https://github.com/node-webrtc/node-webrtc/blob/develop/src/interfaces/rtc_video_sink.cc
audioSink.ondata = ({ samples: { buffer } }) => { | ||
this.pushData(stream, buffer); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 이벤트는 sink 객체에 data가 들어올 때 발생하는 이벤트인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네 특정 단위의 발표자 음성 데이터를 받았을 때 data 이벤트가 발생합니다
const stream = this.peerStreams.get(roomId) as PeerStream; | ||
stream.video.push(Buffer.from(data)); | ||
}; | ||
}; | ||
|
||
pushData = (stream: PeerStream, buffer: ArrayBufferLike) => { | ||
if (!stream.end) { | ||
stream.audio.push(Buffer.from(buffer)); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stream.video.push
는 video 데이터들을 video stream에 저장하는거고, stream.audio.push
는 audio 데이터들을 audio stream에 저장하는거 같은데 audio는 ondata 이벤트가 호출 될 때 저장되고 video는 onframe 이벤트에 호출되나요? ondata와 onframe 이벤트가 어떤 차이가 있는지 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
맞습니다 이벤트 이름만 다를 뿐 역할은 똑같습니다. 아마도 영상은 I420 프레임 단위로 들어오다 보니 data
가 아닌 frame
이벤트로 지은 것 아닐까 싶습니다.
자세한 부분은 해당 링크를 참고해보면 좋을 것 같습니다.
https://github.com/node-webrtc/node-webrtc/blob/develop/docs/nonstandard-apis.md#programmatic-audio
const proc = ffmpeg() | ||
.addInput(videoPath) | ||
.addInputOptions(videoConfig('640x480')) | ||
.addInput(audioPath) | ||
.addInputOptions(audioConfig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ffmpeg input들은 path로 넣어주나보군요..!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네 아까 만들었던 sock 파일 경로를 추가해줍니다
.on('start', () => { | ||
console.log(`${roomId} 강의실 영상 녹화 시작`); | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setFfmpeg
함수가 강의가 종료되고 호출되는 함수같은데 이 start 이벤트가 강의가 시작될 때 실행되는 이벤트인가요? 혹시 이 이벤트가 언제 발생되나요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ffmpeg가 생성될 때(이 코드에서는 102번 라인이 실행됐을 때) start 이벤트가 발생됩니다.
https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#start-ffmpeg-process-started
}; | ||
|
||
mediaStreamToFile = (stream: PassThrough, fileName: string): string => { | ||
const outputPath = path.join(outputDir, `${fileName}.sock`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sock 파일 형식으로 저장하신 이유가 있나요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
보통 프로세스 간 통신을 위한 파일로 sock 확장자를 사용한다고 하더군요 ! 구분하기 위해서 작성했고 다른 의미는 없습니다
fs.unlinkSync(path.join(outputDir, `video-${roomId}.sock`)); | ||
fs.unlinkSync(path.join(outputDir, `audio-${roomId}.sock`)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sync 함수를 쓰신 이유가 있나요?? 특별한 이유가 없으면 그냥 unlink
함수를 쓰는게 좋을 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋습니다 !
const stream = this.peerStreams.get(roomId); | ||
const sinkList = this.peerSinks.get(roomId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네이밍이 stream은 그냥 stream인데 sink는 sink가 아니라 sinkList인 이유가 있나요? 제가 코드 이해한 바로는 peerStreams
나 peerSinks
나 둘다 Map 객체여서 get 하면 객체 하나가 나올 것 같아서요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기서 stream은 발표자의 스트림 정보가 담겨있는 객체(59번 라인)이고, sinkList에는 음성 sink & 영상 sink가 담겨있기 때문에 list를 붙였습니다. 다시보니 직관적이지는 않은 것 같아서 한번 수정해보겠습니다
function end() { | ||
socket.emit('end', {roomId: 1}) | ||
presenterRTCPC.close(); | ||
presenterRTCPC = null; | ||
startButton.disabled = false; | ||
endButton.disabled = true; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 end는 어디서 호출되나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
132번 라인 보시면 endButton(방 나가기)을 눌렀을 때 호출됩니다 !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상세한 설명 감사드립니다! 수정하신다고 하는 부분만 수정해서 올려주시면 머지할게요 👍🏻
발표자의 영상 프레임과 음성 샘플을 저장해둔 임시파일을 병합 후 삭제해야 하는데 이때 비동기로 처리하도록 변경한다.
추후 확장성을 고려하여 영상 및 음성 녹화에 필요한 정보를 클래스 단위로 모듈화했습니다.
미디어 파일 절대 경로 지정 및 임시 파일 삭제 기능 모듈화 적용
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고 많으셨습니다! 머지할게요
작업 개요
작업 사항
강의 종료 시 발표자의 영상과 음성 스트림을 파일로 저장한다 close #180
타입스크립트로 node-webrtc 모듈을 사용하기 위한 타입 정의 파일 추가 close #28
고민한 점들(필수 X)
node 환경에서 지원하는 WebRTC의 타입스크립트 미지원 문제
현재 Node의 WebRTC 모듈은 타입스크립트를 지원하지 않아서, 직접 타입을 지정해줘야 한다.
이전에 WebRTC의 타입을 추가해줬다면 하단 부분을
node_modules/wrtc/lib/index.d.ts
최하단에 추가해준다 (만약 타입을 지정해 준적이 없다면 #120 PR을 참고)WebRTC를 통해 전송되는 미디어 데이터 추출하기
작성 예정
버퍼에 저장된 영상 및 음성 데이터를 하나의 파일로 병합하기
작성 예정