Skip to content

Commit 7dd76dd

Browse files
Add Util Files
Add some util files
1 parent d3b2c33 commit 7dd76dd

8 files changed

Lines changed: 722 additions & 0 deletions

audio_player/__init__.py

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
try:
2+
from . import ui_audioPlayerWidget
3+
except (ImportError, ModuleNotFoundError):
4+
try:
5+
from audio_player import ui_audioPlayerWidget
6+
except (ImportError, ModuleNotFoundError):
7+
import ui_audioPlayerWidget
8+
from PySide6 import QtWidgets, QtCore
9+
import subprocess, json, requests
10+
from ffpyplayer.player import MediaPlayer
11+
from io import BytesIO
12+
import threading
13+
import os, re, signal, time, tempfile
14+
15+
16+
class AudioPlayerWidget(ui_audioPlayerWidget.Ui_Form, QtWidgets.QWidget):
17+
def __init__(self, parent=None):
18+
super().__init__(parent=parent)
19+
self.setupUi(self)
20+
self.media_player = None
21+
self.is_playing = False
22+
self.is_dragging = False
23+
self.is_end = False
24+
self.length = 0.0 # 音频时长(秒)
25+
self.audio_link = ""
26+
self.temp_file_path = "" # 临时文件路径(核心修改)
27+
self.supports_range = False
28+
self.download_thread = None
29+
self.is_downloading = False
30+
31+
self.pushButton_play.clicked.connect(self._play)
32+
self.pushButton_stop.clicked.connect(self.stop)
33+
34+
self.interval_timer = QtCore.QTimer()
35+
self.interval_timer.timeout.connect(self._play_interval)
36+
self.interval_timer.start(100)
37+
38+
self.init_progress_bar()
39+
self.label_download = QtWidgets.QLabel("")
40+
self.layout().addWidget(self.label_download)
41+
42+
def _play(self):
43+
if self.is_playing:
44+
self.pause()
45+
else:
46+
self.play()
47+
48+
def _check_range_support(self, url: str) -> bool:
49+
"""检测URL是否支持部分请求(Range requests)"""
50+
try:
51+
headers = {
52+
"Range": "bytes=0-1",
53+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
54+
}
55+
response = requests.head(
56+
url, headers=headers, allow_redirects=True, timeout=10
57+
)
58+
lower_headers = [k.lower() for k in response.headers.keys()]
59+
return response.status_code == 206 or "accept-ranges" in lower_headers
60+
except Exception as e:
61+
print(f"检测部分请求支持失败: {e}")
62+
return False
63+
64+
def _download_audio_to_tempfile(self, url: str) -> str:
65+
"""下载音频到临时文件(核心修改)"""
66+
try:
67+
self.is_downloading = True
68+
headers = {
69+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
70+
"Accept": "audio/ogg, audio/mpeg, audio/*",
71+
}
72+
73+
response = requests.get(url, headers=headers, stream=True, timeout=10)
74+
response.raise_for_status()
75+
76+
total_size = int(response.headers.get("content-length", 0))
77+
downloaded_size = 0
78+
79+
# 创建临时文件(自动添加音频扩展名)
80+
suffix = ".ogg" if url.lower().endswith(".ogg") else ".mp3"
81+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
82+
temp_path = tmp.name
83+
for chunk in response.iter_content(chunk_size=8192):
84+
if chunk:
85+
tmp.write(chunk)
86+
downloaded_size += len(chunk)
87+
if total_size > 0:
88+
progress = (downloaded_size / total_size) * 100
89+
self.update_download_label(f"下载中: {progress:.1f}%")
90+
91+
self.update_download_label("下载完成,准备播放")
92+
return temp_path # 返回临时文件路径
93+
except Exception as e:
94+
self.update_download_label(f"下载失败: {str(e)}")
95+
print(f"下载音频失败: {e}")
96+
return None
97+
finally:
98+
self.is_downloading = False
99+
100+
def update_download_label(self, text: str):
101+
"""线程安全地更新下载标签"""
102+
QtCore.QMetaObject.invokeMethod(
103+
self.label_download,
104+
"setText",
105+
QtCore.Qt.QueuedConnection,
106+
QtCore.Q_ARG(str, text),
107+
)
108+
109+
def _getlength(self, file_path: str) -> float:
110+
"""从临时文件获取时长(稳定可靠)"""
111+
if not file_path or not os.path.exists(file_path):
112+
return 0.0
113+
114+
# 优先用ffprobe(最可靠)
115+
try:
116+
cmd = [
117+
"ffprobe",
118+
"-v",
119+
"quiet",
120+
"-print_format",
121+
"json",
122+
"-show_format",
123+
file_path,
124+
]
125+
result = subprocess.run(
126+
cmd,
127+
stdout=subprocess.PIPE,
128+
stderr=subprocess.STDOUT,
129+
text=True,
130+
encoding="utf-8",
131+
timeout=10,
132+
)
133+
info = json.loads(result.stdout)
134+
return float(info["format"]["duration"])
135+
except Exception as e:
136+
print(f"ffprobe获取时长失败: {e}")
137+
138+
# 备选:用ffmpeg
139+
try:
140+
cmd = [
141+
"ffmpeg",
142+
"-i",
143+
file_path,
144+
"-v",
145+
"quiet",
146+
"-show_entries",
147+
"format=duration",
148+
"-print_format",
149+
"json",
150+
"pipe:1",
151+
]
152+
result = subprocess.run(
153+
cmd,
154+
stdout=subprocess.PIPE,
155+
stderr=subprocess.STDOUT,
156+
text=True,
157+
encoding="utf-8",
158+
timeout=10,
159+
)
160+
info = json.loads(result.stdout)
161+
return float(info["format"]["duration"])
162+
except Exception as e:
163+
print(f"ffmpeg获取时长失败: {e}")
164+
165+
# 最后:文件大小估算
166+
try:
167+
file_size = os.path.getsize(file_path)
168+
avg_bitrate = 128000 # 128kbps
169+
return (file_size * 8) / avg_bitrate
170+
except Exception as e:
171+
print(f"估算时长失败: {e}")
172+
return 0.0
173+
174+
def load_audio(self, audio_link: str):
175+
"""加载音频(优先临时文件)"""
176+
self.stop() # 停止当前播放
177+
self.audio_link = audio_link
178+
self.temp_file_path = "" # 清空旧临时文件
179+
self.label_status.setText("加载中...")
180+
self.label_download.setText("")
181+
182+
# 网络链接处理
183+
if audio_link.startswith(("http://", "https://")):
184+
self.supports_range = self._check_range_support(audio_link)
185+
186+
if self.supports_range:
187+
# 支持部分请求:直接播放URL
188+
self.media_player = MediaPlayer(audio_link)
189+
self.media_player.set_pause(True)
190+
self.length = self._getlength(audio_link) # 此时file_path是URL
191+
self.label_status.setText("Ready")
192+
else:
193+
# 不支持部分请求:下载到临时文件
194+
self.label_status.setText("不支持部分请求,准备下载...")
195+
self.download_thread = threading.Thread(
196+
target=self._download_and_initialize, args=(audio_link,)
197+
)
198+
self.download_thread.daemon = True
199+
self.download_thread.start()
200+
else:
201+
# 本地文件:直接使用
202+
self.temp_file_path = audio_link # 本地文件路径作为"临时文件"
203+
self.media_player = MediaPlayer(audio_link)
204+
self.media_player.set_pause(True)
205+
self.length = self._getlength(audio_link)
206+
self.label_status.setText("Ready")
207+
208+
def _download_and_initialize(self, audio_link: str):
209+
"""下载到临时文件后初始化播放器"""
210+
temp_path = self._download_audio_to_tempfile(audio_link)
211+
if not temp_path:
212+
QtCore.QMetaObject.invokeMethod(
213+
self,
214+
lambda: self.label_status.setText("下载失败,无法播放"),
215+
QtCore.Qt.QueuedConnection,
216+
)
217+
return
218+
219+
# 保存临时文件路径并初始化播放器
220+
self.temp_file_path = temp_path
221+
QtCore.QMetaObject.invokeMethod(
222+
self, "_post_download_init", QtCore.Qt.QueuedConnection
223+
)
224+
225+
@QtCore.Slot()
226+
def _post_download_init(self):
227+
"""从临时文件初始化播放器"""
228+
if not self.temp_file_path or not os.path.exists(self.temp_file_path):
229+
self.label_status.setText("临时文件不存在")
230+
return
231+
232+
# 直接从临时文件播放
233+
self.media_player = MediaPlayer(self.temp_file_path)
234+
self.media_player.set_pause(True)
235+
self.length = self._getlength(self.temp_file_path)
236+
self.label_status.setText(f"Ready (时长: {self.length:.2f}秒)")
237+
238+
def play(self):
239+
if self.is_downloading or not self.media_player:
240+
return
241+
242+
self.is_end = False
243+
self.is_playing = True
244+
self.media_player.set_pause(False)
245+
self.label_status.setText("Playing...")
246+
self.pushButton_play.setText(";")
247+
248+
def pause(self):
249+
self.is_playing = False
250+
if self.media_player:
251+
self.media_player.set_pause(True)
252+
self.label_status.setText("Paused")
253+
self.pushButton_play.setText("4")
254+
255+
def stop(self, *args, is_end=False):
256+
self.is_playing = False
257+
if self.media_player:
258+
self.media_player.set_pause(True)
259+
if self.media_player.get_pts() > 0:
260+
self.media_player.seek(-self.media_player.get_pts())
261+
if not is_end:
262+
self.label.setText("00:00:00")
263+
self.horizontalSlider.setValue(0)
264+
self.label_status.setText("Ready")
265+
self.pushButton_play.setText("4")
266+
267+
def _play_interval(self):
268+
if (
269+
self.media_player
270+
and self.is_playing
271+
and not self.is_dragging
272+
and not self.is_downloading
273+
):
274+
try:
275+
current_pos = self.media_player.get_pts()
276+
277+
# 动态更新时长(如果初始获取失败)
278+
if self.length <= 0:
279+
self.length = self._getlength(
280+
self.temp_file_path if self.temp_file_path else self.audio_link
281+
)
282+
283+
# 检测播放结束
284+
if self.length > 0 and current_pos >= self.length - 0.5:
285+
self.is_end = True
286+
self.stop(is_end=True)
287+
return
288+
289+
# 更新时间显示
290+
hours = int(current_pos // 3600)
291+
mins = int((current_pos % 3600) // 60)
292+
secs = int(current_pos % 60)
293+
self.label.setText(f"{hours:02d}:{mins:02d}:{secs:02d}")
294+
295+
# 更新进度条
296+
if self.length > 0:
297+
self.horizontalSlider.setValue(
298+
int((current_pos / self.length) * 100)
299+
)
300+
except Exception as e:
301+
print(f"播放更新出错: {e}")
302+
303+
def init_progress_bar(self):
304+
self.horizontalSlider.setRange(0, 100)
305+
self.horizontalSlider.sliderPressed.connect(self.on_slider_pressed)
306+
self.horizontalSlider.sliderReleased.connect(self.on_slider_released)
307+
self.horizontalSlider.valueChanged.connect(self.on_slider_changed)
308+
309+
def on_slider_pressed(self):
310+
self.is_dragging = True
311+
312+
def on_slider_released(self):
313+
if not self.media_player or self.length <= 0:
314+
self.is_dragging = False
315+
return
316+
try:
317+
target_pos = (self.horizontalSlider.value() / 100.0) * self.length
318+
if target_pos >= self.length * 0.99:
319+
self.stop()
320+
else:
321+
current_pos = self.media_player.get_pts()
322+
self.media_player.seek(target_pos - current_pos)
323+
except Exception as e:
324+
print(f"进度条调整出错: {e}")
325+
self.is_dragging = False
326+
327+
def on_slider_changed(self):
328+
if self.is_dragging and self.length > 0:
329+
target_pos = (self.horizontalSlider.value() / 100.0) * self.length
330+
hours = int(target_pos // 3600)
331+
mins = int((target_pos % 3600) // 60)
332+
secs = int(target_pos % 60)
333+
self.label.setText(f"{hours:02d}:{mins:02d}:{secs:02d}")
334+
335+
def closeEvent(self, event):
336+
"""关闭时删除临时文件(核心修改)"""
337+
self.stop()
338+
# 确保临时文件被删除
339+
if self.temp_file_path and os.path.exists(self.temp_file_path):
340+
try:
341+
os.unlink(self.temp_file_path)
342+
print(f"已删除临时文件: {self.temp_file_path}")
343+
except Exception as e:
344+
print(f"删除临时文件失败: {e}")
345+
event.accept()
346+
347+
348+
if __name__ == "__main__":
349+
import sys
350+
351+
qa = QtWidgets.QApplication(sys.argv)
352+
apw = AudioPlayerWidget()
353+
apw.show()
354+
test_url = "http://localhost/Audio_2.ogg" # 替换为实际URL
355+
apw.load_audio(test_url)
356+
qa.exec()
18.9 KB
Binary file not shown.
5.51 KB
Binary file not shown.

0 commit comments

Comments
 (0)