数字投屏叫号器-经由dlna和rtsp实现兼容电子万年历\电视盒子\智能电视\hdml投屏器
目前某些10寸的万年历,电子相册,价格到了100多点,一块屏幕也就这样吧。不论是什么材质的吧,再加50可以支持dlna投屏,还要啥自行车。
我一直在寻找一块可以报号的屏幕和喇叭,就是点餐报号用的,目前成套下来,无线键盘+3位数字的屏幕,也是100上下。
我一般只开1周完成的项目,这次也这样,定时一周,把叫号的内容投屏到电子万年历上。这样不叫号,还能当日历用。现在想来,有空闲了,就叫一个号码,也就是比如餐好了。屏幕就叫 一声,几号餐好。没事的时候,显示默认画面,这就好。
根据AI,ffmeg项目,可以pipen一个管道,python可以生成视频复合音频流。
通过dlna,cast chrome,airplay投送到万年历屏幕。其中dlna应该是兼容最好的。
第一天
2.26先定个目标,生成mpeg的视频流。
2.27
文字转语音是必须的,gtts需要谷歌在线,后来使用pyttsx3
https://blog.youkuaiyun.com/cui_yonghua/article/details/134611001
import pyttsx3
pyttsx3.speak("I will speak this text")
import os
import cv2
import time
import subprocess
#import numpy as np
from pyttsx3 import init
from pydub import AudioSegment
from threading import Thread
class ImageRTSPStreamer:
def __init__(self, img_paths, tts_text, fps=25, rtsp_url="rtsp://localhost:8554/mytts"):
self.img_paths = img_paths
self.tts_text = tts_text
self.fps = fps
self.rtsp_url = rtsp_url
self.audio_ready = False
def generate_audio(self):
"""生成语音并转码为PCM WAV格式"""
tts = gTTS(text=self.tts_text, lang='en')
tts.save("temp_audio.mp3")
engine = init()
engine.setProperty('rate', 150) #速度 默认200
engine.setProperty('volume', 0.9)
engine.save_to_file(self.tts_text, 'temp_audio.mp3')
engine.runAndWait()
# 转换为FFmpeg兼容的音频格式
audio = AudioSegment.from_mp3("temp_audio.mp3")
audio = audio.set_frame_rate(44100).set_channels(1)
audio.export("temp_audio.wav", format="wav")
# 转换到原始的PCM_S16LE格式
subprocess.run([
'ffmpeg', '-y',
'-i', 'temp_audio.wav',
'-f', 's16le',
'-acodec', 'pcm_s16le',
'-ar', '44100',
'audio.raw'
])
self.audio_ready = True
os.remove("temp_audio.mp3")
os.remove("temp_audio.wav")
def send_video(self, pipe):
"""发送图片帧到FFmpeg管道"""
frame_delay = 1 / self.fps
idx = 0
while True:
img = cv2.imread(self.img_paths[idx])
img = cv2.resize(img, (1280, 720))
# YUV420P色彩空间转换
yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV_I420)
pipe.write(yuv.tobytes())
# 更新图片索引
idx = (idx + 1) % len(self.img_paths)
time.sleep(frame_delay)
def start_stream(self):
# 生成音频
t_audio = Thread(target=self.generate_audio)
t_audio.start()
# 启动FFmpeg合成管道
ffmpeg_cmd = [
'ffmpeg',
'-y',
'-f', 'rawvideo', # 原始视频输入格式
'-vcodec','rawvideo',
'-pix_fmt', 'yuv420p',
'-s', '1280x720', # 分辨率与图片预处理一致
'-r', str(self.fps),
'-i', '-', # 从stdin读取视频
'-f', 's16le', # PCM音频输入格式
'-acodec','pcm_s16le',
'-ar', '44100',
'-ac', '1',
'-i', 'audio.raw', # 原始音频文件
'-c:v', 'libx264', # 视频编码器
'-preset', 'ultrafast',
'-tune', 'zerolatency',
'-pix_fmt', 'yuv420p',
'-g', '50', # GOP大小
'-c:a', 'aac', # 音频编码器
'-b:a', '128k',
'-f', 'rtsp', # 输出格式
'-rtsp_transport', 'tcp',# 使用TCP传输降低丢包
self.rtsp_url
]
# 等待音频就绪
while not self.audio_ready:
time.sleep(0.1)
# 启动FFmpeg进程
proc = subprocess.Popen(
ffmpeg_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
# 发送视频帧
self.send_video(proc.stdin)
proc.stdin.close()
proc.wait()
if __name__ == "__main__":
img_folder = ["image1.jpg", "image2.jpg"]
text = "请3号就诊!"
streamer = ImageRTSPStreamer(
img_paths=img_folder,
tts_text=text,
fps=5 # 匹配图片切换速度
)
streamer.start_stream()
哎 opencv 需要 cmake,
2.28
以上代码,因为版本可能有无数错误,折腾一天没有大的成果,
阶段性的,在win10下,pyatv可用使用
import asyncio
import pyatv
async def play_mp4(url):
# 扫描设备
loop = asyncio.get_running_loop()
devices = await pyatv.scan(loop,timeout=5)
if not devices:
print("未找到设备")
return
# 连接设备(假设第一个设备为目标)
for device in devices:
print(f"发现设备: {device}")
atv = await pyatv.connect(devices[0], loop=asyncio.get_event_loop())
try:
# 通过 AirPlay 播放 MP4
await atv.stream.play_url(url)
finally:
atv.close()
# 替换为你的 MP4 文件 URL
asyncio.run(play_mp4("output_video_with_subtitles.mp4"))
可用这样给airplay服务发送, mp4,视频.
另外,大致使用movepy也能生成,text声音和添加文本框.只是速度感人, 大约和视频长度,同样的生成速度.
from moviepy import ImageClip, concatenate_videoclips, TextClip, CompositeVideoClip, AudioFileClip
import os
import subprocess
from pyttsx3 import init
from pydub import AudioSegment
# 图片文件夹路径、音频文件路径和输出视频路径
image_folder = "images" # 替换为包含图片的文件夹路径
audio_path = "audio.mp3" # 替换为音频文件路径
output_path = "output_video_with_text.mp4" # 输出视频路径
# 获取图片文件列表
image_files = [os.path.join(image_folder, img) for img in os.listdir(image_folder) if img.endswith(('.png', '.jpg', '.jpeg'))]
# 创建图片剪辑列表
clips = []
for image_file in image_files:
clip = ImageClip(image_file, duration=2) # 每张图片显示2秒
clips.append(clip)
# 将所有图片剪辑拼接成一个视频
video_clip = concatenate_videoclips(clips, method="compose")
# 加载音频
#audio_clip = AudioFileClip(audio_path)
# 创建动态文本剪辑
def dynamic_text(t):
tts_text= f"请: {int(t)} 号就诊!"
engine = init()
engine.setProperty('rate', 150) #速度 默认200
engine.setProperty('volume', 0.9)
engine.save_to_file(tts_text, audio_path)
engine.runAndWait()
audio = AudioSegment.from_file(audio_path)
audio = audio.set_frame_rate(44100).set_channels(1)
audio.export("temp_audio.wav", format="wav")
# 转换到原始的PCM_S16LE格式
subprocess.run([
'ffmpeg', '-y',
'-i', 'temp_audio.wav',
'-f', 's16le',
'-acodec', 'pcm_s16le',
'-ar', '44100',
'audio.raw'
])
return tts_text
text_clip = TextClip(
text= dynamic_text(45), # 初始文本
font="./simhei.ttf", # 字体类型
color="white", # 文本颜色
stroke_color="black", # 文本边框颜色
stroke_width=1, # 文本边框宽度
size=(video_clip.size[0], None), # 文本宽度与视频宽度一致
method="label" # 使用标签方法渲染文本
)
# 设置文本的位置和持续时间
text_clip = text_clip.with_position(("center", "bottom")).with_duration(video_clip.duration)
# 使用 lambda 函数动态更新文本
#text_clip = text_clip.with_text(lambda t: dynamic_text(t))
# 将文本浮窗添加到视频上
final_clip = CompositeVideoClip([text_clip])
# 写入输出视频文件
final_clip.write_videofile(output_path, codec="libx265", fps=24)
其中AI,给出的代码,TextClip(参数,多数是错的,字体font,需要系统带,txt->text
这是一点进展.
https://sourceforge.net/projects/rtspsimpleserver.mirror/files/latest/download
先在本地建立一个RtspSimpleServer,也就是改名mediamtx的软件,我用的win10
在将格式转为rtmp,搭建mediamtx.exe服务器的时候,出现大问题, 有时 rtsp,和 rtmp 分不清, -f flv 和 -f rtsp,分不清.
ffmpeg -re -stream_loop -1 -i audio.wav -c:a aac -f flv rtmp://localhost:1935/live/stream
这段可用播放本地音频
ffmpeg -re -i 4.mp4 -c:v libx264 -c:a aac -f rtsp rtsp://localhost:8554/myvideo
这段可用播放本地视频
在mediamtx 默认设置下. 然后就是,两相结合.接近成功
3月4号
在实现了rtsp,和rtmp后,无法通过airplay激活电视盒在airReceiver。进行播放。只有mp4可以。现在的问题是 mp4生成速度太慢。如果有个模板,处理新的音频,视频cp,就能很快的更新。但是要更新画面条幅,却做不到。
所以目前就是,向哪个方向努力的问题了。假设可以模拟一个摄像头实现rtsp。因为dlna不支持rtmp,而支持rmsp协议。而airplay同样也支持rtsp协议。
剩下的就是,采用这种适时的音视频生生技术,生成一个媒体地址。然后推送到 播放设备,airplay现在不行,虽然不清楚是为啥,现在可以转为dlna,可能更普遍兼容通常的电子日历设备和android电视盒子。
import cv2
import subprocess
import time
import signal
import sys
import logging
import datetime
# 配置日志记录
logging.basicConfig(
filename='rtsp_stream.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# 常量
IMAGE_PATH = "image1.jpg"
WIDTH, HEIGHT = 640, 480
FPS = 25
RTSP_URL = "rtsp://192.168.1.109:1935/mystream"
FONT = cv2.FONT_HERSHEY_SIMPLEX # 字体
def generate_frame(background, current_time):
"""在背景图上叠加当前时间"""
img = background.copy()
# img = cv2.resize(img, (WIDTH, HEIGHT))
# 获取时间字符串(格式:YYYY-MM-DD HH:MM:SS)
time_str = current_time.strftime("%Y-%m-%d %H:%M:%S")
# 计算文字位置(居中)
text_size = cv2.getTextSize(time_str, FONT, 0.8, 2)[0]
text_x = int(WIDTH / 2 )
text_y = int(HEIGHT - 20) # 底部留20像素边距
# 绘制文字背景框
cv2.rectangle(img,
(text_x - 5, text_y - text_size[1] - 5),
(text_x + text_size[0] + 5, text_y + 5),
(40, 40, 40), -1) # 深灰色填充
# 绘制文字
cv2.putText(img, time_str, (text_x, text_y), FONT, 0.8,
(255, 255, 255), 2, cv2.LINE_AA) # 白色文字
return img
class RTSPStreamer:
def __init__(self):
self.img=self.bg = self._load_image()
self.ffmpeg_process = None
self.running = True # 控制运行标志
def _load_image(self):
"""加载并缩放图片"""
img = cv2.imread(IMAGE_PATH)
if img is None:
logging.error(f"无法加载图片:{IMAGE_PATH}")
raise FileNotFoundError()
return cv2.resize(img, (WIDTH, HEIGHT))
def refresh(self):
return generate_frame(self.bg,datetime.datetime.now())
def _start_ffmpeg(self):
"""启动 FFmpeg 进程"""
command = [
'ffmpeg',
'-y',
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-pix_fmt', 'bgr24',
'-s', f'{WIDTH}x{HEIGHT}',
'-r', str(FPS),
'-i', '-',
'-c:v', 'libx264',
'-preset', 'ultrafast',
'-tune', 'stillimage',
'-rtsp_transport', 'tcp', # 强制TCP提高稳定性
'-f', 'rtsp',
RTSP_URL
]
try:
self.ffmpeg_process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
# stderr=subprocess.PIPE # 捕获错误信息
)
logging.info("FFmpeg 进程已启动")
except Exception as e:
logging.error(f"启动 FFmpeg 失败:{str(e)}")
def _stop_ffmpeg(self):
"""安全关闭 FFmpeg 进程"""
if self.ffmpeg_process and self.ffmpeg_process.poll() is None:
self.ffmpeg_process.stdin.close()
self.ffmpeg_process.terminate()
self.ffmpeg_process.wait(timeout=5)
logging.info("FFmpeg 进程已关闭")
def _stream_loop(self):
"""持续发送帧数据"""
while self.running:
try:
# 检查进程状态,若退出则重启
if self.ffmpeg_process is None or self.ffmpeg_process.poll() is not None:
self._stop_ffmpeg()
self._start_ffmpeg()
time.sleep(1) # 稍等再继续
if self.ffmpeg_process.poll() is not None:
continue # 重启失败则跳过当前循环
# 发送帧数据
self.ffmpeg_process.stdin.write(self.refresh().tobytes())
time.sleep(1/FPS)
except (BrokenPipeError, IOError) as e:
logging.error(f"流写入失败:{str(e)},尝试重启进程...")
self._stop_ffmpeg()
except Exception as e:
logging.error(f"未知错误:{str(e)}")
self.running = False
def run(self):
"""运行主循环"""
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
self._start_ffmpeg()
self._stream_loop()
def _signal_handler(self, sig, frame):
"""处理退出信号(如 Ctrl+C)"""
logging.info("接收到终止信号,清理资源...")
self.running = False
self._stop_ffmpeg()
sys.exit(0)
if __name__ == "__main__":
streamer = RTSPStreamer()
streamer.refresh()
streamer.run()
以上代码会不停的更新时间, 中断后会继续,但是播放端不能自动重连。
找到一个rust在开源项目可以作为 rtsp 。xiu
cargo install xiu
xiu -t 1935 这是开启 rtsp的服务的。
叠加动态字符的写法
ffmpeg -re -stream_loop -1 -i "input.mp4" -vf "drawtext=textfile=text.txt:reload=1:x=10:y=10:fontsize=20:fontcolor=red" -c:v libx264 -preset ultrafast -f rtsp "rtsp://192.168.1.109:1935/mystream"
通过添加 字体:fontsize=20:fontcolor=red:fontfile=simhei.ttf 显示中文,
这个-vf是视频滤镜,可以定义很多. 主要是,这里的text.txt.可以动态更改, 延时生效.
这是使用了 drawtext的滤镜,
那么动态语音,怎么绑定呢?
0305
通过调查声音,在linux平台下,可以
-f concat -safe 0 -i audio_list.txt 通过FIFO这种队列, 指定声音列表,然后里面定义音频文件的顺序
file audio.wav
file audio2.wav
然而window平台是需要,反复重启ffmpeg.这造成的瞬断是无法忍受的,因为编码的时间不确定, dlna,和airplay,也无法持续.
所以,采用虚拟声卡,作为声音的输入. 通过,往声卡发送播放指令来发送实时的音频.
https://vb-audio.com/Cable/
ffmpeg -f dshow -i audio="CABLE Output (VB-Audio Virtual Cable)" -acodec aac -b:a 128k -f rtsp rtsp://your-rtsp-server/stream
嗯这样确实可以输出电脑音频,
下面是,一个循环视频+ 声卡输出的合成rtsp. 然后可以发送到播放设备.
ffmpeg -re -stream_loop -1 -i 4.mp4 -f dshow -i audio="CABLE Output (VB-Audio Virtual Cable)" -acodec aac -b:a 128k -c:v libx264 -preset ultrafast -map 0:v -map 1:a -f rtsp rtsp://192.168.1.100:8554/live
下面是集合了动态文本的:"
-vf "drawtext=textfile=text.txt:reload=1:x=10:y=10:fontsize=20:fontcolor=red:fontfile=simhei.ttf"
这个参数需要在两个输入参数之后
ffmpeg -re -stream_loop -1 -i 4.mp4 -f dshow -i audio="CABLE Output (VB-Audio Virtual Cable)" -acodec aac -b:a 128k -vf "drawtext=textfile=text.txt:reload=1:x=10:y=10:fontsize=20:fontcolor=red:fontfile=simhei.ttf" -c:v libx264 -preset ultrafast -map 0:v -map 1:a -f rtsp rtsp://192.168.1.100:8554/live
这样就完成了初步的功能架构.平时播放某个MP4.当有空闲,播放就诊客户编号,叫号,并且更新text.txt文件,在屏幕上进行文本提示. 同时播放一定次数的pyttsx3的文生语音到 VB-audio 的虚拟声卡,它的输出,直接投射 外面大厅的屏幕.
下面是发送到屏幕(万年历).
import asyncio
import pyatv
async def play_mp4(url):
# 扫描设备
loop = asyncio.get_running_loop()
devices = await pyatv.scan(loop,timeout=5)
if not devices:
print("未找到设备")
return
# 连接设备(假设第一个设备为目标)
for device in devices:
print(f"发现设备: {device}")
atv = await pyatv.connect(devices[0], loop=asyncio.get_event_loop())
try:
# 通过 AirPlay 播放 MP4
await atv.stream.play_url(url)
finally:
atv.close()
# 替换为你的 MP4 文件 URL
asyncio.run(play_mp4("rtsp://192.168.1.100:8554/live"))
之所以这样搞,是为了,在电脑端形成广播,发送到万年历. 提醒用户就诊
声卡设置
编程呼叫:
>>> import pyttsx3
>>> eng=pyttsx3.init()
>>> eng.say("hello")
>>> eng.runAndWait()
>>> eng.say("请3号就诊")
>>> eng.runAndWait()
>>> eng.say("请13号就诊")
>>> eng.runAndWait()
3月6日
尝试了dlna的场景, 作为低端的设备 hls 这种延时的流可以播放,不符合要求.rtsp的部分设备不支持,可能包括那个电子日历. 折中方案,
- 使用电视盒子或手机, 安装投屏软件airReceiver
https://www.cr173.com/soft/1588150.html
这个没有广告,支持airplay, 建议用airplay,在我低配的letv可以使用.dlna 也可以但是卡. - 我发现vlc开源软件可以安装在window,或者android上,它可以打开要使用的rtsp流.然后持续播放.
以下是dlna方式的播放代码.唯一好处是不需要操作播放设备
import subprocess
import time
import upnpclient
import socket
def get_local_ip():
try:
# 创建一个 UDP 套接字
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 连接到一个外部地址(这里使用 Google 的 DNS 服务器)
s.connect(("8.8.8.8", 80))
# 获取本地 IP 地址
local_ip = s.getsockname()[0]
finally:
# 关闭套接字
s.close()
return local_ip
# 查找 DLNA 设备
def find_dlna_device():
devices = upnpclient.discover()
if not devices:
print("未找到 DLNA 设备")
return None
# 选择第一个支持媒体播放的设备
for device in devices:
if "AVTransport" in device.service_map:
print(f"找到 DLNA 设备: {device.friendly_name}:地址:{device.location}")
if device.location.find("102")>0:
# print(device.AVTransport.GetDeviceCapabilities(InstanceID=0))
return device
print("未找到支持媒体播放的 DLNA 设备")
return None
# 播放 RTSP 流
def play_rtsp_stream(device, to_url):
av_transport = device.AVTransport
# 设置播放 URL
# media_url = rtsp_url #"http://192.168.1.100:8080" # ffmpeg 输出的 HTTP 流
av_transport.SetAVTransportURI(
InstanceID=0,
CurrentURI=to_url,
CurrentURIMetaData=""
)
print("Device supports the protocol.")
# 开始播放
av_transport.Play(InstanceID=0, Speed="1")
if __name__ == "__main__":
myip=get_local_ip()
hls_url = f"http://{myip}:8888/live/index.m3u8" # 替换为你的 RTSP 流 URL 为hls流
http_url= f"http://{myip}:8888/live" # 可以http浏览器播放
rtsp_url=f"rtsp://{myip}:8554/live"
#
srt_url=f"srt://{myip}:8890?streamid=read:myvideo&pkt_size=1316"
to_url=rtsp_url
print('ffplay ',to_url)
# 查找 DLNA 设备
device = find_dlna_device()
if device:
# 播放 RTSP 流
play_rtsp_stream(device,to_url)
else:
print("未找到 DLNA 设备")
用单个图片生成rtsp,可以随意编辑,适时更新
ffmpeg -re -framerate 30 -f image2 -loop 1 -i "image1.jpg" -c:v libx264 -preset ultrafast -tune zerolatency -pix_fmt rgba -f rtsp -rtsp_transport tcp rtsp://localhost:1935/live
rgba是色彩模式,