隨著AIGC和數(shù)字人技術的成熟韧掩,實時數(shù)字人也迎來了廣泛使用,目前在娛樂窖铡、社交、教育坊谁、直播费彼、客服領域都有很好地落地場景。
如果想實現(xiàn)數(shù)字人的實時性口芍,除了數(shù)字人模型本身箍铲,工程上還要解決兩個關鍵點:一個是如何將服務端生成的數(shù)字人展現(xiàn)到客戶/用戶端,另一個是在此基礎上實現(xiàn)實時性鬓椭,盡量減少用戶的等待時間颠猴。
數(shù)字人展現(xiàn)給前端用戶一般有兩種技術方案关划。一種是傳遞音頻流和嘴部數(shù)據(jù),由前端進行渲染翘瓮,渲染一般用到Live2D贮折、Unreal Engine或者Unity等,通常適用于卡通形象的數(shù)字人资盅;另一種是直接由服務端完成渲染调榄,通過視頻推流的方式推送給前端用戶觀看,通常適用于真人復刻形式的數(shù)字人呵扛,本文主要講解的是第二種技術方案每庆。
音視頻推流一般使用RTC技術,但目前常見的RTC技術今穿,或者服務商提供的SDK缤灵,均只實現(xiàn)了客戶端之間的通信,即采集本地攝像頭或傳遞某個視頻文件蓝晒,通過RTC/RTMP協(xié)議發(fā)送給接收端凤价。通過調(diào)研發(fā)現(xiàn)FFmpeg具備推流能力,我們就可以在服務端調(diào)用FFmpeg拔创,將數(shù)字人的音視頻流推送到RTC服務利诺。FFmpeg是一款強大的多媒體處理軟件,支持各種視頻處理操作剩燥,包括混流慢逾、推流等。
使用FFmpeg推流很簡單灭红,以下命令就實現(xiàn)了一個mp4文件的推流
ffmpeg -re -i input.mp4 -c copy -f flv rtmp://server/live/streamName
如果想將視頻和音頻混流推送侣滩,可以使用以下命令:
ffmpeg -re -y -an -i input.mp4 -i input.wav -c copy -f flv rtmp://server/live/streamName
那如何結合FFmpeg實現(xiàn)實時推流呢?這就需要在數(shù)字人模塊生成每一幀圖像的同時变擒,同步將該幀圖像和對應的音頻采樣數(shù)據(jù)君珠,通過FFmpeg推流到RTMP。我們可以設置兩個管道娇斑,一個接收視頻幀數(shù)據(jù),一個接收音頻采樣數(shù)據(jù)毫缆,分別有兩個線程向兩個管道勻速寫入唯竹,以實現(xiàn)模擬直播推流的效果苦丁,具體實現(xiàn)代碼如下:
import cv2
import subprocess
import time
import numpy as np
import librosa
import threading
import os
import hashlib
import asyncio
# 將視頻流寫入管道
def write_video_stream(cap, fps, pipe_name):
fd_pipe = os.open(pipe_name, os.O_WRONLY)
while True:
ret, frame = cap.read()
if not ret:
break
os.write(fd_pipe, frame.tobytes())
os.close(fd_pipe)
# 將音頻流寫入管道;
def write_audio_stream(cap, speech_array, fps, pipe_name):
fd_pipe = os.open(pipe_name, os.O_WRONLY)
wav_frame_num = int(44100 / fps)
while True:
# 由于音頻流的采樣率是44100, 而視頻流的幀率是30, 因此需要對音頻流進行分幀
speech = speech_array[frame_counter * wav_frame_num : (frame_counter+1) * wav_frame_num]
os.write(fd_pipe, speech.tostring())
frame_counter += 1
# 根據(jù)視頻幀數(shù)決定音頻寫入次數(shù)
if frame_counter == int(cap.get(cv2.CAP_PROP_FRAME_COUNT)):
break
os.close(fd_pipe)
def push():
# 模擬數(shù)字人生成的視頻流和音頻流
# 使用OpenCV讀取視頻流
cap = cv2.VideoCapture("input.mp4")
# 使用librosa讀取音頻流
speech_array, sr = librosa.load("input.wav", sr=44100) # 對于rtmp, 音頻速率是有要求的,這里采用了44100
speech_array = (speech_array*32767).astype(np.int16) # 轉為整型
push_url = 'rtmp://xxxx.com/live/stream_name'
# 獲取視頻流的幀率、寬度和高度
fps = float(cap.get(5))
width = int(cap.get(3))
height = int(cap.get(4))
# 創(chuàng)建兩個"named pipes"棵磷,用于存放視頻流和音頻流
# 判斷如果管道存在,則先unlink
if os.path.exists('video_pipe'):
os.unlink('video_pipe')
if os.path.exists('audio_pipe'):
os.unlink('audio_pipe')
os.mkfifo('video_pipe')
os.mkfifo('audio_pipe')
# ffmpeg命令仪媒,不做詳解,可以參考ffmpeg文檔
command = ['ffmpeg',
'-loglevel', 'info',
'-y', '-an',
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-pix_fmt', 'bgr24',
'-s', "{}x{}".format(width, height),
'-r', str(fps),
'-i', 'video_pipe', # 視頻流管道作為輸入
'-f', 's16le',
'-acodec', 'pcm_s16le',
'-i', 'audio_pipe', # 音頻流管道作為輸入
'-c:v', "libx264",
'-pix_fmt', 'yuv420p',
'-s', "960x540",
'-preset', 'ultrafast',
'-profile:v', 'baseline',
'-tune', 'zerolatency',
'-g', '2',
'-b:v', "1000k",
'-ac', '1',
'-ar', '44100',
'-acodec', 'aac',
'-shortest',
'-f', 'flv',
push_url]
# 啟動進程運行ffmpeg命令
proc = subprocess.Popen(command, shell=False, stdin=subprocess.PIPE)
# 創(chuàng)建兩個線程规丽,分別將視頻流和音頻流寫入"named pipes"
video_thread = threading.Thread(target=write_video_stream, args=(cap, fps, 'video_pipe'))
audio_thread = threading.Thread(target=write_audio_stream, args=(cap, speech_array, fps, 'audio_pipe'))
video_thread.start()
audio_thread.start()
video_thread.join()
audio_thread.join()
proc.wait()
# Remove the "named pipes".
os.unlink('video_pipe')
os.unlink('audio_pipe')
if __name__ == "__main__":
push()
這里并未真正接入數(shù)字人模塊,通過cv2讀取視頻文件每一幀模擬赌莺。正式接入方式,可以開啟兩個隊列松嘶,由數(shù)字人模塊分別寫入音頻幀和視頻幀,兩個線程分別從隊列中讀取音頻幀和視頻幀數(shù)據(jù)翠订,再寫入pipe管道即可。
參考文獻:
包包凱:通過python實時生成音視頻數(shù)據(jù)并通過ffmpeg推送和混流
https://stackoverflow.com/questions/74256808/how-to-merge-audio-and-video-in-bytes-using-ffmpeg