일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- Golang
- kafka-connect
- tls disable
- minikube
- elastic
- offset
- partition
- loadbalance
- kafka broker
- create topic
- Producer
- command
- k8s
- Message
- 쿠버네티스
- consumer group
- Kafka Connect
- Kafka
- broker
- Helm
- es
- Consumer
- Produce
- ElasticSearch
- kibana
- eck
- topic
- http
- Kubernetes
- Elk
- Today
- Total
개발자의 개발괴발
[RTSP] Save streaming video 본문
cctv 영상을 스트리밍받아 내 pc에 저장해보자.
rtsp에 지식이 전무하다보니 시간이 꽤 걸렸다.
사전준비
go에서 rtps를 사용하기 위해 gortsplib를 사용했다.
go get "github.com/bluenviron/gortsplib/v4"
깃헙에 가면 example도 잘 되어있다.
그런데 ffmpeg 버전을 낮은 버전을 사용한것 같다.
몇몇의 example은 cgo를 써야하는데 몇몇 함수가 deprecated된것도 있고 해서 c라이브러리를 쓰는 곳에선 잘 동작하진 않았다.
cgo를 쓰기 위해선 ffmpeg도 설치해야한다.(위 패키지를 설치할때 같이 설치 되는지는 잘 모르겠다.)
설치하기 위해서 homebrew로 설치하자.
brew install ffmpeg
streaming할 cctv영상 원본은 여기서 구할 수 있다.
국가에서 제공해주는 CCTV 영상들인데 처음 두개의 rtsp는 작동하지 않았다. 그래서 난 세번째 것을 사용했다.
구현하기
package main
import (
"log"
"os"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
)
func main() {
c := gortsplib.Client{}
// parse URL
u, err := base.ParseURL("rtsp://210.99.70.120:1935/live/cctv003.stream")
if err != nil {
panic(err)
}
// connect to the server
err = c.Start(u.Scheme, u.Host)
if err != nil {
panic(err)
}
defer c.Close()
// find available medias
desc, _, err := c.Describe(u)
if err != nil {
panic(err)
}
// setup all medias
err = c.SetupAll(desc.BaseURL, desc.Medias)
if err != nil {
panic(err)
}
// H264 포맷 찾기
var forma *format.H264
medi := desc.FindFormat(&forma)
if medi == nil {
log.Fatal("H264 포맷을 찾을 수 없습니다")
}
// RTP 디코더 생성
rtpDec, err := forma.CreateDecoder()
if err != nil {
log.Fatal("RTP 디코더 생성 실패:", err)
}
// output.h264 파일 생성
f, err := os.Create("output.h264")
if err != nil {
log.Fatal("파일 생성 실패:", err)
}
defer f.Close()
// called when a RTP packet arrives
c.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
log.Printf("RTP packet from media %v\n", medi)
// RTP 패킷 디코딩하여 NALU 추출
nalus, err := rtpDec.Decode(pkt)
if err != nil {
// 일부 패킷은 여러 RTP 패킷이 필요할 수 있으므로 무시
if err == rtph264.ErrMorePacketsNeeded || err == rtph264.ErrNonStartingPacketAndNoPrevious {
log.Println("RTP 패킷 버려짐:", err)
return
}
log.Println("RTP 디코딩 오류:", err)
return
}
// NALU를 Annex-B 포맷으로 변환하여 파일에 저장
for _, nalu := range nalus {
f.Write([]byte{0x00, 0x00, 0x00, 0x01})
f.Write(nalu)
}
})
// called when a RTCP packet arrives
c.OnPacketRTCPAny(func(medi *description.Media, pkt rtcp.Packet) {
log.Printf("RTCP packet from media %v, type %T\n", medi, pkt)
})
// start playing
_, err = c.Play(nil)
if err != nil {
panic(err)
}
// wait until a fatal error
panic(c.Wait())
}
만들고보니 별거 아니란 생각이 들었지만 한참 걸렸다. GPT의 도움도 많이 받았다.
rtsp에 대해서는 나중에 더 알아보기로 하고 구현한 것에 대해 설명을 추가하겠다.
// parse URL
u, err := base.ParseURL("rtsp://210.99.70.120:1935/live/cctv003.stream")
url이 아래와 같이 나눠지게 된다.
u.Scheme: rtsp
u.Host: 210.99.70.120:1935
desc, _, err := c.Describe(u)
서버에 연결을 하고 Describe을 해보면 description.Session이 반환되게 되고 아래와 같은 정보를 얻을 수 있다.
Medias를 주목해볼 필요가 있을것 같은데 여러개의 media를 한번에 streaming되는 것처럼 보인다.
지금은 1개의 Media만 있지만 소리도 같이 나오면 audio도 될 것 같고 application type도 있다.
gortpslib에선 아래와 같이 정의하고 있다.
// media types.
const (
MediaTypeVideo MediaType = "video"
MediaTypeAudio MediaType = "audio"
MediaTypeApplication MediaType = "application"
)
format을 보면 우리가 받으려고 하는 cctv 데이터는 H264 타입인것 같다.
var forma *format.H264
rtpDec, err := forma.CreateDecoder()
패킷을 decode하려면 decoder가 있어야 한다.
c.OnPacketRTPAny(func(medi *description.Media, forma format.Format, pkt *rtp.Packet) {
log.Printf("RTP packet from media %v\n", medi)
})
c.OnPacketRTCPAny(func(medi *description.Media, pkt rtcp.Packet) {
log.Printf("RTCP packet from media %v, type %T\n", medi, pkt)
})
위 두 함수는 Pack이 client에 도달했을때 호출되는 callback 함수를 등록한다.
하나는 RTP이고 하나는 RTCP인데
RTP는 실제 streaming data이고
RTCP는 control하는 data이다.
_, err = c.Play(nil)
이렇게 Play를 해줘야 데이터가 온다.
처음에 Play를 해주지 않아서 host와 연결되었는데 데이터가 안오길래 원인이 뭔지 한참 찾아 헤맸다.
이렇게 하고 돌려보면 아래와 같이 저장되었다.
저장된 파일이 같은데 플레이어에 따라 조금 다르게 나온다.
VLC로 재생하면 뚝뚝 끊기는 지점이 있는데 이유는 모르겠다.... 좀 더 공부해봐야겠다.
if err == rtph264.ErrMorePacketsNeeded || err == rtph264.ErrNonStartingPacketAndNoPrevious {
log.Println("RTP 패킷 버려짐:", err)
return
}
이쪽 부분으로 빠지는 패킷이 많은데 이것때문인지는 잘 모르겠다.
GPT는 아래처럼 답해주더라...
추가로 GPT에 물어보니 해결할 수 있었다.
ffmpeg -framerate 25 -i output.h264 -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p output.mp4
추가 설명은 아래와 같다.
-framerate의 값은 아래처럼 ffprobe해주면 마지막줄에 fps 값이 나온다. 그 값을 넣어주면 된다.
$ ffprobe output.h264
...
Input #0, h264, from 'output.h264':
Duration: N/A, bitrate: N/A
Stream #0:0: Video: h264 (Baseline), yuv420p(progressive), 640x360, 25 fps, 1200k tbr, 1200k tbn
- 아래는 GPT 선생의 설명이다.
🧩 각 옵션 설명:
-framerate 30
- 입력 스트림의 프레임 레이트를 30fps로 지정합니다.
- H.264 원본에는 보통 FPS 정보가 없기 때문에 명시적으로 지정해야 속도 문제가 안 생깁니다.
-i input.h264
- 입력 파일입니다. 이 경우 raw H.264 스트림을 의미합니다.
-c:v libx264
- 비디오 코덱을 libx264로 설정 (H.264로 재인코딩).
- copy가 아닌 이유는 타임스탬프 문제 해결 및 포맷 호환성을 위해 재인코딩하는 것입니다.
-preset fast
- 인코딩 속도와 압축 효율 간의 트레이드오프 조절.
- ultrafast, superfast, veryfast, fast, medium, slow, veryslow 등이 있으며,
- 빠를수록 인코딩은 빠르지만 파일 용량이 커짐.
- 느릴수록 용량은 줄고 화질이 좋아지지만 시간이 더 걸림.
-crf 23
- 화질 조절을 위한 주요 옵션 (Constant Rate Factor).
- 범위: 0 (무손실) ~ 51 (최악의 화질).
- 일반적으로 18~28 사이 사용, 낮을수록 고화질/고용량.
- 23은 기본값으로, 무난한 품질/용량 균형.
-pix_fmt yuv420p
- 픽셀 포맷을 yuv420p로 설정.
- 대부분의 플레이어 및 웹에서 가장 호환성 좋은 포맷.
- yuv444p 등 다른 포맷도 있지만, 호환성이 떨어질 수 있어 일반적으로 yuv420p 권장.
output.mp4
- 최종 출력 파일 이름이며, MP4 컨테이너로 저장됩니다.