数字投屏叫号器-经由dlna和rtsp实现兼容电子万年历\电视盒子\智能电视\hdml投屏器

数字投屏叫号器-经由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的部分设备不支持,可能包括那个电子日历. 折中方案,

  1. 使用电视盒子或手机, 安装投屏软件airReceiver
    https://www.cr173.com/soft/1588150.html
    这个没有广告,支持airplay, 建议用airplay,在我低配的letv可以使用.dlna 也可以但是卡.
  2. 我发现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是色彩模式,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wjcroom

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值