import sys
import time
import threading
from fractions import Fraction
import av
import numpy as np
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
RTMP_URL = 'rtmp://127.0.0.1:1935/live/test'
VIDEO_WIDTH = 1280
VIDEO_HEIGHT = 720
VIDEO_FPS = 30
VIDEO_TIME_BASE = Fraction(1, VIDEO_FPS)
AUDIO_RATE = 24000
AUDIO_CHANNELS = 1
AUDIO_SAMPLES = 1024
AUDIO_TIME_BASE = Fraction(1, AUDIO_RATE)
VIDEO_CODEC = 'h264_nvenc'
class RTMPStreamer:
def __init__(self, url):
self.url = url
self.running = False
self.thread = None
self.container = None
def start(self):
if self.running:
return
self.running = True
self.thread = threading.Thread(target=self._run, name='RTMPStreamer', daemon=True)
self.thread.start()
def stop(self):
self.running = False
def _run(self):
print('子线程启动')
try:
print('准备 av.open()')
self.container = av.open(
self.url,
'w',
format='flv',
timeout=(3.0, 3.0),
)
print('av.open() 成功')
video_stream = self.container.add_stream(VIDEO_CODEC, rate=VIDEO_FPS)
video_stream.width = VIDEO_WIDTH
video_stream.height = VIDEO_HEIGHT
video_stream.pix_fmt = 'nv12'
video_stream.time_base = VIDEO_TIME_BASE
video_stream.options = {
'preset': 'p1',
'tune': 'll',
'bf': '0',
'b': '2500k',
}
audio_stream = self.container.add_stream('aac', rate=AUDIO_RATE)
audio_stream.layout = 'mono'
audio_stream.time_base = AUDIO_TIME_BASE
audio_stream.options = {
'b': '64k',
}
video = np.zeros((VIDEO_HEIGHT, VIDEO_WIDTH, 3), dtype=np.uint8)
audio = np.zeros(AUDIO_SAMPLES, dtype=np.int16)
video_pts = 0
audio_pts = 0
next_video_time = time.monotonic()
while self.running:
now = time.monotonic()
if now >= next_video_time:
frame = av.VideoFrame.from_ndarray(video, format='bgr24')
frame.pts = video_pts
frame.time_base = VIDEO_TIME_BASE
video_pts += 1
for packet in video_stream.encode(frame):
self.container.mux(packet)
next_video_time += 1.0 / VIDEO_FPS
audio_frame = av.AudioFrame.from_ndarray(
audio.reshape(1, -1),
format='s16',
layout='mono',
)
audio_frame.sample_rate = AUDIO_RATE
audio_frame.pts = audio_pts
audio_frame.time_base = AUDIO_TIME_BASE
audio_pts += AUDIO_SAMPLES
for packet in audio_stream.encode(audio_frame):
self.container.mux(packet)
sleep_time = next_video_time - time.monotonic()
if sleep_time > 0:
time.sleep(min(sleep_time, 0.01))
print('开始 flush')
for packet in video_stream.encode():
self.container.mux(packet)
for packet in audio_stream.encode():
self.container.mux(packet)
print('推流线程正常结束')
except Exception as e:
import traceback
traceback.print_exc()
print('推流异常:', repr(e))
finally:
container = self.container
self.container = None
if container is not None:
try:
container.close()
except Exception as e:
print('关闭异常:', repr(e))
self.running = False
print('子线程退出')
class Demo(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('PyAV RTMP 阻塞测试')
self.resize(400, 180)
self.label = QLabel('状态:未推流')
self.button = QPushButton('推流')
self.button.clicked.connect(self.on_push)
layout = QVBoxLayout(self)
layout.addWidget(self.label)
layout.addWidget(self.button)
self.streamer = RTMPStreamer(RTMP_URL)
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_ui)
self.timer.start(100)
self.ui_tick = 0
def on_push(self):
print('主线程:点击推流')
self.label.setText('状态:正在连接...')
self.button.setEnabled(False)
self.streamer.start()
def update_ui(self):
self.ui_tick += 1
self.label.setText(f'状态:UI正常运行 {self.ui_tick}')
def closeEvent(self, event):
self.streamer.stop()
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Demo()
window.show()
sys.exit(app.exec())
When the target network is unreachable, the main thread will be blocked
python3.13
pyav==15.1.0(many versions will be blocked)
pyside6==6.8.2