Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
ethan
/
appframe
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Commit b617e011
authored
2026-11-05 22:20:42 +0800
by
ethanlamzs
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
优化播放失败后
1 parent
f8375082
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
446 additions
and
71 deletions
lib/services/player_service.dart
lib/services/player_service.dart
View file @
b617e01
...
...
@@ -20,6 +20,27 @@ class PlayerService {
/// 当前播放文件的时长
int
?
_playDuration
;
/// 播放代次,每次发起新播放自增,用于识别陈旧的 whenFinished 回调
int
_playSeq
=
0
;
/// 轻量串行锁:串行化 playAudio / stopAudio,防止并发调用时播放器重建逻辑交错
Future
<
void
>
_playLock
=
Future
.
value
();
/// 远程音频下载超时
static
const
Duration
_downloadTimeout
=
Duration
(
seconds:
10
);
/// HttpClient 建连超时
static
const
Duration
_connectTimeout
=
Duration
(
seconds:
10
);
/// startPlayer / openPlayer 兜底超时(防插件内部 Future 永久悬挂死锁)
static
const
Duration
_startTimeout
=
Duration
(
seconds:
10
);
/// 常规操作(暂停/恢复/进度/倍速/停止/关闭)死锁防护超时
static
const
Duration
_opTimeout
=
Duration
(
seconds:
3
);
/// 当前播放的远程音频对应的本地临时文件
String
?
_localAudioPath
;
Function
?
_sendResponse
;
set
sendResponse
(
Function
value
)
{
...
...
@@ -29,6 +50,19 @@ class PlayerService {
Future
<
bool
>
_initPlayer
()
async
{
// 打开播放器
try
{
// 幂等清理:防止旧订阅泄漏、旧实例未关闭(异常重建场景会重复进入本方法)
await
_playerSubscription
?.
cancel
();
_playerSubscription
=
null
;
try
{
if
(
_player
!=
null
)
{
// 死锁防护:旧实例内部锁可能已被悬挂的 startPlayer 持有,超时后放弃(接受泄漏)
await
_withTimeout
(
_player
!.
closePlayer
(),
_opTimeout
);
}
}
catch
(
e
)
{
print
(
e
);
}
_player
=
null
;
// 针对iOS, 需要控制不使用听筒进行播放
if
(
Platform
.
isIOS
)
{
var
audioSession
=
await
AudioSession
.
instance
;
...
...
@@ -38,10 +72,10 @@ class PlayerService {
final
player
=
FlutterSoundPlayer
();
_player
=
(
await
player
.
openPlayer
(
))!;
_player
=
(
await
_withTimeout
(
player
.
openPlayer
(),
_startTimeout
))!;
// 播放进度回调
_player
!.
setSubscriptionDuration
(
Duration
(
seconds:
1
)
);
await
_withTimeout
(
_player
!.
setSubscriptionDuration
(
Duration
(
seconds:
1
)),
_opTimeout
);
_playerSubscription
=
_player
!.
onProgress
!.
listen
((
event
)
{
_playDuration
=
event
.
duration
.
inSeconds
;
var
playPosition
=
event
.
position
.
inSeconds
;
...
...
@@ -63,73 +97,348 @@ class PlayerService {
}
}
Future
<
bool
>
playAudio
(
String
url
,
int
seek
,
String
playId
,
double
playRate
)
async
{
if
(!(
_playerIsInit
??
false
))
{
final
initResult
=
await
_initPlayer
();
if
(!
initResult
)
{
return
false
;
}
/// 轻量串行锁:串行化 playAudio / stopAudio,防止并发调用时播放器重建逻辑交错
Future
<
T
>
_withLock
<
T
>(
Future
<
T
>
Function
()
action
)
{
final
result
=
_playLock
.
then
((
_
)
=>
action
());
// 链尾吞掉异常,保证后续排队操作不被跳过;异常仍通过 result 传给调用方
_playLock
=
result
.
then
((
_
)
{},
onError:
(
_
)
{});
return
result
;
}
/// 对底层插件调用加超时兜底:插件内部锁可能被悬挂的 startPlayer 永久持有,
/// 超时后放弃等待并走重建流程(底层 future 迟到完成/出错不会成为未捕获异常)
Future
<
T
>
_withTimeout
<
T
>(
Future
<
T
>
future
,
Duration
timeout
)
{
future
.
ignore
();
return
future
.
timeout
(
timeout
);
}
/// 是否为需要自行下载的远程音频地址。
/// 插件 iOS 远程分支存在"下载错误被吞、startPlayer 永久悬挂无异常"的缺陷,
/// 远程音频一律先下载到本地临时文件再播放
bool
_isRemoteUrl
(
String
url
)
{
final
uri
=
Uri
.
tryParse
(
url
);
return
uri
!=
null
&&
(
uri
.
scheme
==
'http'
||
uri
.
scheme
==
'https'
)
&&
uri
.
host
.
isNotEmpty
;
}
/// 从 URL 提取安全的文件扩展名(白名单校验,缺省空串)
String
_extFromUrl
(
String
url
)
{
final
path
=
Uri
.
tryParse
(
url
)?.
path
??
''
;
final
idx
=
path
.
lastIndexOf
(
'.'
);
if
(
idx
<
0
)
{
return
''
;
}
final
ext
=
path
.
substring
(
idx
).
toLowerCase
();
final
ok
=
RegExp
(
r'^\.[a-z0-9]+$'
).
hasMatch
(
ext
)
&&
ext
.
length
<=
10
;
return
ok
?
ext
:
''
;
}
// 针对iOS, 需要控制不使用听筒进行播放
if
(
Platform
.
isIOS
)
{
final
session
=
await
AudioSession
.
instance
;
await
session
.
configure
(
AudioSessionConfiguration
.
music
());
/// 通过文件头魔数识别音频真实格式,返回扩展名(无法识别返回空串)。
/// 实测存在 URL/Content-Type 标注 mp3 但实际内容是 WAV 的服务端,
/// 错误的扩展名会误导 AVAudioPlayer 选择错误的解析器导致播放失败
Future
<
String
>
_sniffAudioExt
(
File
file
)
async
{
try
{
final
head
=
await
file
.
openRead
(
0
,
16
).
first
;
String
str
(
int
start
,
int
end
)
=>
String
.
fromCharCodes
(
head
.
skip
(
start
).
take
(
end
-
start
));
if
(
head
.
length
>=
12
&&
str
(
0
,
4
)
==
'RIFF'
&&
str
(
8
,
12
)
==
'WAVE'
)
{
return
'wav'
;
}
if
(
head
.
length
>=
3
&&
str
(
0
,
3
)
==
'ID3'
)
{
return
'mp3'
;
}
if
(
head
.
length
>=
4
&&
str
(
0
,
4
)
==
'OggS'
)
{
return
'ogg'
;
}
if
(
head
.
length
>=
4
&&
str
(
0
,
4
)
==
'fLaC'
)
{
return
'flac'
;
}
if
(
head
.
length
>=
4
&&
str
(
0
,
4
)
==
'caff'
)
{
return
'caf'
;
}
if
(
head
.
length
>=
8
&&
str
(
4
,
8
)
==
'ftyp'
)
{
return
'm4a'
;
}
// ADTS AAC 帧头(FF F1 / FF F9)
if
(
head
.
length
>=
2
&&
head
[
0
]
==
0xFF
&&
(
head
[
1
]
&
0xF6
)
==
0xF0
)
{
return
'aac'
;
}
// MPEG 音频帧同步(FF FB / FF F3 等)
if
(
head
.
length
>=
2
&&
head
[
0
]
==
0xFF
&&
(
head
[
1
]
&
0xE0
)
==
0xE0
)
{
return
'mp3'
;
}
}
catch
(
e
)
{
print
(
e
);
}
return
''
;
}
/// best-effort 删除文件,永不抛异常
Future
<
void
>
_deleteFileQuietly
(
String
?
path
)
async
{
if
(
path
==
null
||
path
.
isEmpty
)
{
return
;
}
try
{
final
file
=
File
(
path
);
if
(
await
file
.
exists
())
{
await
file
.
delete
();
}
}
catch
(
e
)
{
print
(
e
);
}
}
// // 获取当前音频会话配置
// AudioSession session = await AudioSession.instance;
// if (session.isConfigured) {
// AudioSessionConfiguration config = session.configuration!;
//
// // 打印音频会话信息
// debugPrint('音频会话配置: ${config.toString()}');
// debugPrint('音频模式: ${config.avAudioSessionMode ?? "N/A"}');
// debugPrint('音频类别: ${config.avAudioSessionCategory ?? "N/A"}');
// } else {
// debugPrint('还未配置音频会话');
// }
/// 下载远程音频到本地临时文件;失败抛异常(此时播放器实例未被触碰,完全健康)
Future
<
String
>
_downloadToTemp
(
String
url
)
async
{
final
client
=
HttpClient
()
..
connectionTimeout
=
_connectTimeout
..
autoUncompress
=
true
;
File
?
tempFile
;
try
{
final
request
=
await
client
.
getUrl
(
Uri
.
parse
(
url
));
// 带 上浏览器 UA:部分 CDN/防盗链会拒绝非浏览器 UA(dart 默认 UA 会被返回错误页或空内容)
request
.
headers
.
set
(
HttpHeaders
.
userAgentHeader
,
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) '
'AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'
);
final
response
=
await
request
.
close
().
timeout
(
_downloadTimeout
);
final
contentType
=
response
.
headers
.
value
(
HttpHeaders
.
contentTypeHeader
)
??
''
;
print
(
'音频下载: HTTP
${response.statusCode}
, type=
$contentType
, contentLength=
${response.contentLength}
'
);
if
(
response
.
statusCode
!=
200
)
{
throw
Exception
(
'HTTP
${response.statusCode}
'
);
}
// 服务器返回了文本页面(常见于防盗链/鉴权失败),并非音频数据
if
(
contentType
.
contains
(
'text/'
)
||
contentType
.
contains
(
'application/json'
))
{
throw
Exception
(
'服务器返回非音频内容:
$contentType
'
);
}
// if (Platform.isIOS) {
// var currentRoute = await AVAudioSession().currentRoute;
// debugPrint('当前音频路由-------------------: ${currentRoute.outputs}');
// }
tempFile
=
File
(
'
${Directory.systemTemp.path}
/appframe_audio_
${DateTime.now().millisecondsSinceEpoch}${_extFromUrl(url)}
'
);
await
response
.
pipe
(
tempFile
.
openWrite
()).
timeout
(
_downloadTimeout
);
final
size
=
await
tempFile
.
length
();
print
(
'音频下载完成:
$size
bytes ->
${tempFile.path}
'
);
if
(
size
==
0
)
{
throw
Exception
(
'文件内容为空'
);
}
_playId
=
playId
;
// 关键修复:按文件头魔数识别真实格式并修正扩展名,
// AVAudioPlayer 依赖扩展名选择解析器(URL 标 mp3 实为 WAV 时会解析失败)
final
sniffedExt
=
await
_sniffAudioExt
(
tempFile
);
final
curExt
=
_extFromUrl
(
tempFile
.
path
);
if
(
sniffedExt
.
isNotEmpty
&&
curExt
!=
'.
$sniffedExt
'
)
{
final
newPath
=
'
${Directory.systemTemp.path}
/appframe_audio_
${DateTime.now().millisecondsSinceEpoch}
.
$sniffedExt
'
;
tempFile
=
await
tempFile
.
rename
(
newPath
);
print
(
'格式识别: 实际内容为
$sniffedExt
(原扩展名 "
$curExt
"),已重命名 ->
$newPath
'
);
}
return
tempFile
.
path
;
}
catch
(
e
)
{
// 清理下载半成品
try
{
await
tempFile
?.
delete
();
}
catch
(
e2
)
{
print
(
e2
);
}
throw
Exception
(
'音频下载失败:
$e
'
);
}
finally
{
// force 关闭:超时场景真正掐断连接,取消后台下载
client
.
close
(
force:
true
);
}
}
/// 单次启动播放(设置速度 + 启动 + 回填时长),供首次尝试与重建后重试复用
Future
<
void
>
_startOnce
(
FlutterSoundPlayer
player
,
String
url
,
double
playRate
,
String
playId
,
int
seq
)
async
{
// 播放速度
_player
!.
setSpeed
(
playRate
);
await
_withTimeout
(
player
.
setSpeed
(
playRate
),
_opTimeout
);
final
duration
=
await
_withTimeout
(
player
.
startPlayer
(
fromURI:
url
,
whenFinished:
_buildWhenFinished
(
player
,
playId
,
seq
),
),
_startTimeout
,
);
// startPlayer 会返回音轨总时长,立即回填,避免超短音频结束时取不到时长
if
(
duration
!=
null
)
{
_playDuration
=
duration
.
inSeconds
;
}
}
await
_player
!.
startPlayer
(
fromURI:
url
,
whenFinished:
()
async
{
await
_player
!.
stopPlayer
();
/// 构造播放结束回调:带 playId + 播放代次快照,防止旧播放晚到的结束回调误杀新播放
void
Function
()
_buildWhenFinished
(
FlutterSoundPlayer
player
,
String
playId
,
int
seq
)
{
return
()
async
{
// 陈旧回调防御:播放指令已切换到新内容,放弃本次结束处理
if
(
playId
!=
_playId
||
seq
!=
_playSeq
)
{
return
;
}
// 补发一下全部进度
var
h5Cmd
=
{
'unique'
:
''
,
'cmd'
:
'audioProgress'
,
'data'
:
{
'playId'
:
_playId
,
'duration'
:
_playDuration
,
'currentTime'
:
_playDuration
},
'errMsg'
:
''
,
};
_sendResponse
!(
h5Cmd
);
// 使用闭包捕获的局部实例停止,避免误操作到重建后的新实例
try
{
await
_withTimeout
(
player
.
stopPlayer
(),
_opTimeout
);
}
catch
(
e
)
{
// 注意:插件可能抛出 String 类型错误,必须用裸 catch
print
(
e
);
}
// 播放结束后,发送消息给客户端
h5Cmd
=
{
'unique'
:
''
,
'cmd'
:
'audioEnd'
,
'data'
:
{
'playId'
:
_playId
},
'errMsg'
:
''
,
};
_sendResponse
!(
h5Cmd
);
},
);
if
(
seek
!=
0
)
{
await
seekAudio
(
seek
);
// 补发一下全部进度
var
h5Cmd
=
{
'unique'
:
''
,
'cmd'
:
'audioProgress'
,
'data'
:
{
'playId'
:
_playId
,
'duration'
:
_playDuration
,
'currentTime'
:
_playDuration
},
'errMsg'
:
''
,
};
_sendResponse
!(
h5Cmd
);
// 播放结束后,发送消息给客户端
h5Cmd
=
{
'unique'
:
''
,
'cmd'
:
'audioEnd'
,
'data'
:
{
'playId'
:
_playId
},
'errMsg'
:
''
,
};
_sendResponse
!(
h5Cmd
);
};
}
/// 尽力重建播放器(销毁旧实例 + 重新初始化),永不抛异常
Future
<
bool
>
_tryRebuildPlayer
()
async
{
try
{
await
_initPlayer
();
return
(
_playerIsInit
??
false
)
&&
_player
!=
null
;
}
catch
(
e
)
{
print
(
'重建播放器失败:
$e
'
);
return
false
;
}
return
true
;
}
/// 复位到干净的未初始化状态,保证下次播放重新走初始化流程,永不抛异常
Future
<
void
>
_resetToUninitialized
()
async
{
try
{
await
_playerSubscription
?.
cancel
();
}
catch
(
e
)
{
print
(
e
);
}
_playerSubscription
=
null
;
try
{
if
(
_player
!=
null
)
{
// 死锁防护:超时后放弃旧实例(接受泄漏)
await
_withTimeout
(
_player
!.
closePlayer
(),
_opTimeout
);
}
}
catch
(
e
)
{
print
(
e
);
}
_player
=
null
;
_playerIsInit
=
false
;
_playId
=
''
;
_playDuration
=
null
;
// 清理本地临时音频文件
await
_deleteFileQuietly
(
_localAudioPath
);
_localAudioPath
=
null
;
}
Future
<
bool
>
playAudio
(
String
url
,
int
seek
,
String
playId
,
double
playRate
)
async
{
return
_withLock
(()
async
{
// 增强判断:状态标志与实例不一致(异常后可能出现的脏状态)时也重新初始化
if
(!(
_playerIsInit
??
false
)
||
_player
==
null
)
{
final
initResult
=
await
_initPlayer
();
if
(!
initResult
)
{
return
false
;
}
}
// 针对iOS, 需要控制不使用听筒进行播放
if
(
Platform
.
isIOS
)
{
final
session
=
await
AudioSession
.
instance
;
await
session
.
configure
(
AudioSessionConfiguration
.
music
());
}
// // 获取当前音频会话配置
// AudioSession session = await AudioSession.instance;
// if (session.isConfigured) {
// AudioSessionConfiguration config = session.configuration!;
//
// // 打印音频会话信息
// debugPrint('音频会话配置: ${config.toString()}');
// debugPrint('音频模式: ${config.avAudioSessionMode ?? "N/A"}');
// debugPrint('音频类别: ${config.avAudioSessionCategory ?? "N/A"}');
// } else {
// debugPrint('还未配置音频会话');
// }
// if (Platform.isIOS) {
// var currentRoute = await AVAudioSession().currentRoute;
// debugPrint('当前音频路由-------------------: ${currentRoute.outputs}');
// }
// 远程音频先自行下载到本地临时文件再播放:
// 绕开插件 iOS 远程分支"下载错误被吞、startPlayer 永久悬挂且无异常"的缺陷
//(即线上 WAVEAudioFile::OpenFromDataSource Failed 后无法再播放的根因)。
// 放在 _playId/_playSeq 更新之前,下载失败时零状态污染,旧播放继续且语义正确。
String
playUrl
=
url
;
String
?
downloadedPath
;
if
(
_isRemoteUrl
(
url
))
{
downloadedPath
=
await
_downloadToTemp
(
url
);
playUrl
=
downloadedPath
;
}
// 换曲清理目标:上一个已无人引用的临时文件
final
previousLocalPath
=
_localAudioPath
;
_playId
=
playId
;
// 播放代次自增,用于识别陈旧的 whenFinished 回调
_playSeq
+=
1
;
// 重置上一首的时长,避免污染本次播放的进度上报
_playDuration
=
null
;
final
player
=
_player
!;
try
{
await
_startOnce
(
player
,
playUrl
,
playRate
,
playId
,
_playSeq
);
}
catch
(
e
)
{
// 底层启动失败(如 iOS 报 WAVEAudioFile::OpenFromDataSource Failed)后,
// 播放器实例可能已损坏,这里销毁重建后重试一次,保证不影响后续新播放
print
(
'startPlayer 失败,尝试重建播放器恢复:
$e
'
);
// 下载文件诊断:辅助排查下载内容不是音频/被防盗链篡改的情况
// (HTML 页面头 3c 21 44 4f、gzip 头 1f 8b、MP3 头 ff fb / 49 44 33、WAV 头 52 49 46 46)
if
(
downloadedPath
!=
null
)
{
try
{
final
file
=
File
(
downloadedPath
);
final
size
=
await
file
.
length
();
final
head
=
await
file
.
openRead
(
0
,
16
).
first
;
final
hex
=
head
.
map
((
b
)
=>
b
.
toRadixString
(
16
).
padLeft
(
2
,
'0'
)).
join
(
' '
);
print
(
'下载文件诊断: size=
$size
bytes, 前16字节=
$hex
'
);
}
catch
(
e3
)
{
print
(
e3
);
}
}
final
recovered
=
await
_tryRebuildPlayer
();
if
(!
recovered
)
{
await
_resetToUninitialized
();
// 清理本次下载的文件(可能损坏)
await
_deleteFileQuietly
(
downloadedPath
);
rethrow
;
}
try
{
// 重试继续使用同一本地文件,不重复下载
await
_startOnce
(
_player
!,
playUrl
,
playRate
,
playId
,
_playSeq
);
}
catch
(
e2
)
{
print
(
'重建播放器后重试仍失败:
$e2
'
);
await
_resetToUninitialized
();
await
_deleteFileQuietly
(
downloadedPath
);
// 抛出原始异常,保持对外错误内容不变
throw
e
;
}
}
// 新播放已成功启动(旧播放已被 startPlayer 内部停止,旧文件无人引用),清理并登记
await
_deleteFileQuietly
(
previousLocalPath
);
_localAudioPath
=
downloadedPath
;
if
(
seek
!=
0
)
{
await
seekAudio
(
seek
);
}
return
true
;
});
}
Future
<
bool
>
pauseAudio
()
async
{
...
...
@@ -137,11 +446,19 @@ class PlayerService {
throw
Exception
(
"播放器未初始化"
);
}
if
(
_player
==
null
)
{
throw
Exception
(
"播放器未初始化"
);
}
if
(!
_player
!.
isPlaying
)
{
throw
Exception
(
"播放器状态错误"
);
}
await
_player
!.
pausePlayer
();
try
{
await
_withTimeout
(
_player
!.
pausePlayer
(),
_opTimeout
);
}
on
TimeoutException
catch
(
e
)
{
throw
Exception
(
'音频暂停超时:
$e
'
);
}
return
true
;
}
...
...
@@ -150,17 +467,29 @@ class PlayerService {
throw
Exception
(
"播放器未初始化"
);
}
if
(
_player
==
null
)
{
throw
Exception
(
"播放器未初始化"
);
}
if
(!
_player
!.
isPaused
)
{
throw
Exception
(
"播放器状态错误"
);
}
if
(
playRate
!=
null
)
{
_player
!.
setSpeed
(
playRate
);
try
{
await
_withTimeout
(
_player
!.
setSpeed
(
playRate
),
_opTimeout
);
}
on
TimeoutException
catch
(
e
)
{
throw
Exception
(
'设置播放速度超时:
$e
'
);
}
// 设置速度之后,会自动恢复播放
return
true
;
}
await
_player
!.
resumePlayer
();
try
{
await
_withTimeout
(
_player
!.
resumePlayer
(),
_opTimeout
);
}
on
TimeoutException
catch
(
e
)
{
throw
Exception
(
'音频恢复超时:
$e
'
);
}
return
true
;
}
...
...
@@ -169,7 +498,15 @@ class PlayerService {
throw
Exception
(
"播放器未初始化"
);
}
await
_player
!.
seekToPlayer
(
Duration
(
seconds:
seek
));
if
(
_player
==
null
)
{
throw
Exception
(
"播放器未初始化"
);
}
try
{
await
_withTimeout
(
_player
!.
seekToPlayer
(
Duration
(
seconds:
seek
)),
_opTimeout
);
}
on
TimeoutException
catch
(
e
)
{
throw
Exception
(
'音频进度跳转超时:
$e
'
);
}
return
true
;
}
...
...
@@ -178,30 +515,59 @@ class PlayerService {
throw
Exception
(
"播放器未初始化"
);
}
if
(
_player
==
null
)
{
throw
Exception
(
"播放器未初始化"
);
}
// await pauseAudio();
// await resumeAudio(playRate);
_player
!.
setSpeed
(
playRate
);
try
{
await
_withTimeout
(
_player
!.
setSpeed
(
playRate
),
_opTimeout
);
}
on
TimeoutException
catch
(
e
)
{
throw
Exception
(
'设置播放速度超时:
$e
'
);
}
return
true
;
}
Future
<
bool
>
stopAudio
()
async
{
if
(!(
_playerIsInit
??
false
))
{
throw
Exception
(
"播放器未初始化"
);
}
return
_withLock
(()
async
{
if
(!(
_playerIsInit
??
false
))
{
throw
Exception
(
"播放器未初始化"
);
}
if
(!
_player
!.
isPlaying
&&
!
_player
!.
isPaused
)
{
throw
Exception
(
"播放器状态错误
"
);
}
if
(
_player
==
null
)
{
throw
Exception
(
"播放器未初始化
"
);
}
await
_player
!.
stopPlayer
();
return
true
;
if
(!
_player
!.
isPlaying
&&
!
_player
!.
isPaused
)
{
throw
Exception
(
"播放器状态错误"
);
}
try
{
await
_withTimeout
(
_player
!.
stopPlayer
(),
_opTimeout
);
}
on
TimeoutException
catch
(
e
)
{
// 超时死锁防护:底层实例可能已损坏,尽力重建,保证不影响下次新播放
print
(
'stopPlayer 超时,尝试重建播放器:
$e
'
);
await
_tryRebuildPlayer
();
throw
Exception
(
'音频停止超时:
$e
'
);
}
catch
(
e
)
{
// 停止失败时底层实例可能已损坏,尽力重建,保证不影响下次新播放
print
(
'stopPlayer 异常,尝试重建播放器:
$e
'
);
await
_tryRebuildPlayer
();
rethrow
;
}
return
true
;
});
}
Future
<
bool
>
clearAudio
()
async
{
try
{
await
_player
?.
closePlayer
();
if
(
_player
!=
null
)
{
// 死锁防护:超时后放弃旧实例(接受泄漏)
await
_withTimeout
(
_player
!.
closePlayer
(),
_opTimeout
);
}
_player
=
null
;
await
_playerSubscription
?.
cancel
();
_playerSubscription
=
null
;
...
...
@@ -211,6 +577,9 @@ class PlayerService {
_playerIsInit
=
false
;
_playId
=
''
;
_playDuration
=
null
;
// 清理本地临时音频文件
await
_deleteFileQuietly
(
_localAudioPath
);
_localAudioPath
=
null
;
return
true
;
}
...
...
@@ -219,7 +588,10 @@ class PlayerService {
///
Future
<
void
>
close
()
async
{
try
{
await
_player
?.
closePlayer
();
if
(
_player
!=
null
)
{
// 死锁防护:超时后放弃旧实例(接受泄漏)
await
_withTimeout
(
_player
!.
closePlayer
(),
_opTimeout
);
}
_player
=
null
;
await
_playerSubscription
?.
cancel
();
_playerSubscription
=
null
;
...
...
@@ -229,5 +601,8 @@ class PlayerService {
_playerIsInit
=
false
;
_playId
=
''
;
_playDuration
=
null
;
// 清理本地临时音频文件
await
_deleteFileQuietly
(
_localAudioPath
);
_localAudioPath
=
null
;
}
}
Write
Preview
Styling with
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment