周末折騰了一下這個(gè)海思機(jī)頂盒,家里還有個(gè)海思的攝像機(jī)模組開發(fā)板,結(jié)合機(jī)頂盒來(lái)做個(gè)錄像。
準(zhǔn)備工作
攝像機(jī)模組和機(jī)頂盒都接入路由器的LAN口,確保網(wǎng)絡(luò)正常通信。
道具
攝像機(jī)模組
調(diào)試錄像
攝像機(jī)模組
攝像機(jī)模組里的程序其實(shí)是基于海思的SDK里的demo稍作修改而成,沒有做太復(fù)雜的功能,只加入了RTSP,對(duì)外提供RTSP接口服務(wù)。
這里用的rtsp服務(wù)的庫(kù)代碼比較好用,源碼鏈接:https://gitee.com/fensnote/RtspServer
在電腦上用VLC測(cè)試?yán)鞑シ牛?/p>
VLC
海納斯盒子錄像
關(guān)于錄像,這里只是實(shí)現(xiàn)簡(jiǎn)單的文件存儲(chǔ)、循環(huán)覆蓋,并不是專業(yè)的錄像,專業(yè)錄像里會(huì)做的比較復(fù)雜。
- 1. 直接用Ffmpeg命令行錄取數(shù)據(jù)到文件里,為了方便播放保存為MP4文件。2. 寫代碼實(shí)現(xiàn)rtsp拉流存儲(chǔ),可以自己定義傳參。
Ffmpeg錄像
這個(gè)比較簡(jiǎn)單,一條命令即可,不過(guò)直接采用命令錄像沒法指定實(shí)現(xiàn)循環(huán)覆蓋,要想實(shí)現(xiàn)可以再寫個(gè)腳本取定時(shí)檢測(cè)錄像文件的個(gè)數(shù)。
首先需要先下載安裝Ffmpeg:
sudo?apt install ffmpeg
安裝日志
我這里已經(jīng)安裝過(guò)了。
接下來(lái)就用可以執(zhí)行錄像了:
ffmpeg -rtsp_transport tcp -i rtsp://192.168.2.168:41667/live -c copy -f segment -segment_time 60 stream_piece_%d.mp4
這條命令里是指定了錄像時(shí)長(zhǎng)60秒,即一分鐘切換一個(gè)文件。
ffmpeg
如下截圖,錄取一分鐘后已切換文件,1分鐘錄像數(shù)據(jù)15M,數(shù)據(jù)量挺大了:
錄像上傳上來(lái)播放一下看看:
上傳
播放:
使用win11的系統(tǒng)播放器就可以播放
播放
寫個(gè)代碼錄像
這里選用了go語(yǔ)言來(lái)寫這個(gè)錄像代碼,是因?yàn)間o語(yǔ)言的音視頻、網(wǎng)絡(luò)相關(guān)的庫(kù)實(shí)在太多,比較好用,代碼量也不大,可以提需求讓AI去寫,AI寫的基本上稍作修改測(cè)試幾次就可以用了。
Go還有個(gè)好處就是靜態(tài)編譯,真正的跨平臺(tái),一次編譯,CPU架構(gòu)一樣都可以運(yùn)行,感覺缺點(diǎn)就是可執(zhí)行文件比較大。
這里采用的gortsplib,源碼地址:https://gitee.com/fensnote/gortsplib.git
可以基于gortsplib/examples下的client-play-format-h264-save-to-disk示例代碼做修改:
復(fù)制
我復(fù)制了命名為client-play-format-h264-save-to-disk-file,在這里修改,下面代碼是調(diào)試完成的代碼:
package?main
import?(
? ??"flag"
? ??"fmt"
? ??"log"
? ??"os"
? ??"os/signal"
? ??"syscall"
? ??"time"
? ??"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/format/rtph264"
? ??"github.com/pion/rtp"
)
const?(
? ? filePrefix =?"rec"? ??// 文件名前綴
? ? fileSuffix =?".mp4"? ?// 文件名后綴
)
func?main()?{
? ??// Define command line flags
? ? rtspURL := flag.String("r",?"",?"RTSP URL")
? ? maxFilesPtr := flag.Int("c",?0,?"文件數(shù)")
? ? startFileNumPtr := flag.Int("s",?0,?"起始文件編號(hào)")
? ? durationPtr := flag.Int("t",?60,?"單個(gè)文件錄像時(shí)長(zhǎng)")
? ? modePtr := flag.String("m",?"loop",?"錄像模式,單次模式:"once",循環(huán)模式:"loop", 注意要加雙引號(hào)")
? ??// Parse command line flags
? ? flag.Parse()
? ??// Check if the required arguments are provided
? ??if?*rtspURL ==?""?|| *maxFilesPtr ==?0?{?//|| *startFileNumPtr == 0 ? ??
? ? ? ? flag.PrintDefaults()?// Print usage information
? ? ? ? log.Fatal("Missing required command line arguments")
? ? }
? ??if?*startFileNumPtr <?0?|| *startFileNumPtr > *maxFilesPtr {
? ? ? ? log.Fatalf("起始文件編號(hào)必須是0~%d", *maxFilesPtr)
? ? }
? ??// Check if the RTSP URL is valid
? ? u, err := base.ParseURL(*rtspURL)
? ??if?err !=?nil?{
? ? ? ? log.Fatalf("無(wú)效的RTSP URL: %v", err)
? ? }
? ? c := gortsplib.Client{}
? ??// Connect to the server
? ? err = c.Start(u.Scheme, u.Host)
? ??if?err !=?nil?{
? ? ? ? log.Fatalf("連接 RTSP server 失敗: %v", err)
? ? }
// ? ?defer c.Close()
? ??// Find available medias
? ? desc, _, err := c.Describe(u)
? ??if?err !=?nil?{
? ? ? ? log.Fatalf("Failed to describe RTSP stream: %v", err)
? ? }
? ??// Find the H264 media and format
? ??var?forma *format.H264
? ? medi := desc.FindFormat(&forma)
? ??if?medi ==?nil?{
? ? ? ? log.Fatal("H264 media not found")
? ? }
? ??// Setup RTP -> H264 decoder
? ? rtpDec, err := forma.CreateDecoder()
? ??if?err !=?nil?{
? ? ? ? log.Fatalf("Failed to create H264 decoder: %v", err)
? ? }
? ??var?mpegtsMuxer *mpegtsMuxer
? ??var?fileCounter?int
? ??var?recordingStartTime time.Time
? ??// var bakPts int64;
? ??// var sub int
? ??// Create the first file immediately when the program starts
? ? fileCounter = *startFileNumPtr
? ? newFileName := fmt.Sprintf("%s%03d%s", filePrefix, fileCounter, fileSuffix)
? ? mpegtsMuxer = newMpegtsMuxer(newFileName, forma.SPS, forma.PPS)
? ? err = mpegtsMuxer.initialize()
? ??if?err !=?nil?{
? ? ? ? log.Fatalf("Failed to initialize MPEG-TS muxer: %v", err)
? ? }
? ? log.Printf("New file created: %s", newFileName)
? ? recordingStartTime = time.Now()
? ??// Setup a single media
? ? _, err = c.Setup(desc.BaseURL, medi,?0,?0)
? ??if?err !=?nil?{
? ? ? ? log.Fatalf("Failed to setup media: %v", err)
? ? }
? ??// Create a ticker to create a new file based on the specified duration
? ? duration := time.Duration(*durationPtr) * time.Second
? ? ticker := time.NewTicker(duration)
? ? duration = duration +?100000000// Add 200ms to the duration to ensure the ticker fires after the duration
? ??defer?ticker.Stop()
? ??// bakPts = 0
? ??// Called when a RTP packet arrives
? ? c.OnPacketRTP(medi, forma,?func(pkt *rtp.Packet)?{
? ? ? ??// Decode timestamp
? ? ? ? pts, ok := c.PacketPTS2(medi, pkt)
? ? ? ??if?!ok {
? ? ? ? ? ??//log.Printf("Waiting for timestamp")
? ? ? ? ? ? pts =?int64(pkt.Timestamp)
? ? ? ? ? ??//return
? ? ? ? }
? ? ? ??// if bakPts == 0 {
? ? ? ??// ? ? bakPts = pts
? ? ? ??// }
? ? ? ??// Extract access unit from RTP packets
? ? ? ? au, err := rtpDec.Decode(pkt)
? ? ? ??if?err !=?nil?{
? ? ? ? ? ??if?err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded {
? ? ? ? ? ? ? ? log.Printf("ERR: %v", err)
? ? ? ? ? ? }
? ? ? ? ? ??return
? ? ? ? }
? ? ? ??
? ? ? ??// sub = (int)(pts - bakPts)/100000
? ? ? ??// Encode the access unit into MPEG-TS
? ? ? ??if?mpegtsMuxer !=?nil?{
? ? ? ? ? ? err = mpegtsMuxer.writeH264(au, pts)
? ? ? ? ? ??if?err !=?nil?{
? ? ? ? ? ? ? ? log.Printf("ERR: %v", err)
? ? ? ? ? ? ? ??return
? ? ? ? ? ? }
? ? ? ? ? ??// log.Printf("Saved TS packet, pts: %d,sub:%d",pts,sub)
? ? ? ? }
? ? ? ??// Check if it's time to create a new file or exit
? ? ? ??// if sub >= *durationPtr {
? ? ? ??if?time.Since(recordingStartTime) >= duration {
? ? ? ? ? ? mpegtsMuxer.close()
? ? ? ? ? ??if?*modePtr ==?"once"?{
? ? ? ? ? ? ? ? log.Println("Recording duration reached, exiting...")
? ? ? ? ? ? ? ? c.Close()?// Close the RTSP client connection
? ? ? ? ? ? ? ? os.Exit(0)?// Exit the program immediately
? ? ? ? ? ? }?else?{
? ? ? ? ? ? fileCounter = (fileCounter +?1) % *maxFilesPtr
? ? ? ? ? ? newFileName := fmt.Sprintf("%s%03d%s", filePrefix, fileCounter, fileSuffix)
? ? ? ? ? ? mpegtsMuxer = newMpegtsMuxer(newFileName, forma.SPS, forma.PPS)
? ? ? ? ? ? err = mpegtsMuxer.initialize()
? ? ? ? ? ??if?err !=?nil?{
? ? ? ? ? ? ? ? log.Fatalf("ERR: %v", err)
? ? ? ? ? ? ? ? c.Close()?// Close the RTSP client connection
? ? ? ? ? ? ? ? os.Exit(-1)?// Exit the program immediately
? ? ? ? ? ? }
? ? ? ? ? ? log.Printf("New file created: %s", newFileName)
? ? ? ? ? ? recordingStartTime = time.Now()
? ? ? ? ? ??//bakPts = pts
? ? ? ? }
? ? ? ? }
? ? })
? ??// Start playing
? ? _, err = c.Play(nil)
? ??if?err !=?nil?{
? ? ? ? log.Fatalf("Failed to play RTSP stream: %v", err)
? ? }
? ??// Wait for interrupt signal or recording duration
? ? sigChan :=?make(chan?os.Signal,?1)
? ? signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
? ??gofunc()?{
? ? ? ??for?{
? ? ? ? ? ??select?{
? ? ? ? ? ??case?<-ticker.C:
? ? ? ? ? ? ? ??if?*modePtr ==?"once"?{
? ? ? ? ? ? ? ? ? ? log.Println("Recording duration reached, exiting...")
? ? ? ? ? ? ? ? ? ? c.Close()
? ? ? ? ? ? ? ? ? ? os.Exit(0)
? ? ? ? ? ? ? ? }
? ? ? ? ? ??case?<-sigChan:
? ? ? ? ? ? ? ? log.Println("Interrupt signal received, exiting...")
? ? ? ? ? ? ? ? c.Close()
? ? ? ? ? ? ? ? os.Exit(0)
? ? ? ? ? ? }
? ? ? ? }
? ? }()
? ??// Block main goroutine forever
? ??select?{}
}
代碼編譯:
export?GOOS=linux
export?GOARCH=arm
export?GOARM=5
#export CGO_ENABLED=1
go build -ldflags?'-s -w'
錄像測(cè)試:
?vrec
? -c int
? ? ? ? 文件數(shù)
? -m string
? ? ? ? 錄像模式,單次模式:"once",循環(huán)模式:"loop", 注意要加雙引號(hào) (default?"loop")
? -r string
? ? ? ? RTSP URL
? -s int
? ? ? ? 起始文件編號(hào)
? -t int
? ? ? ? 單個(gè)文件錄像時(shí)長(zhǎng) (default 60)
2025/05/10 09:30:59 Missing required?command?line arguments
#錄像命令參數(shù):
vrec -c 1200 -m?"loop"?-s 0 -t 60 ?-r rtsp://192.168.2.168:41667/live
錄像文件播放
錄像文件查看,這是錄了一晚上的,文件比較多:
測(cè)試
通過(guò)電腦查看
在海納思的內(nèi)置web頁(yè)面查看錄像文件,首頁(yè)還是挺好看的:
首頁(yè)
文件管理器
錄像文件
錄像文件可以直接點(diǎn)擊播放:
通過(guò)手機(jī)查看
手機(jī)
文件管理
錄像列表
點(diǎn)擊播放播放