Skip to content

Commit ad58d86

Browse files
AnonymousAnonymous
authored andcommitted
chore: add diagnostics for haptics and offline downloads
1 parent 449dd5f commit ad58d86

4 files changed

Lines changed: 228 additions & 11 deletions

File tree

ios/Runner/AppDelegate.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ class AudioHapticsBridge {
4343
private var engine: CHHapticEngine?
4444
private var impactGenerator: UIImpactFeedbackGenerator?
4545
private var streamAnalysisGeneration = 0
46+
private var loggedHapticsCapability = false
47+
private var loggedCoreHapticsFailure = false
48+
private var loggedFallbackPulse = false
49+
private var loggedReadableAlias = false
4650

4751
init(controller: FlutterViewController) {
4852
channel = FlutterMethodChannel(
@@ -166,6 +170,10 @@ class AudioHapticsBridge {
166170
try? FileManager.default.removeItem(at: readableURL.url)
167171
}
168172
}
173+
if readableURL.shouldRemove && !loggedReadableAlias {
174+
sendDiagnostic("iOS 分析文件没有可识别扩展名,已创建临时别名: \(readableURL.url.lastPathComponent)")
175+
loggedReadableAlias = true
176+
}
169177
let url = readableURL.url
170178
let file = try AVAudioFile(forReading: url)
171179
let sourceFormat = file.processingFormat
@@ -425,9 +433,25 @@ class AudioHapticsBridge {
425433
}
426434
}
427435

436+
private func sendDiagnostic(_ message: String) {
437+
DispatchQueue.main.async { [weak self] in
438+
self?.channel.invokeMethod("diagnostic", arguments: ["message": message])
439+
}
440+
}
441+
428442
private func pulse(intensity: Double, durationMs: Int) {
429443
let clampedIntensity = max(0.1, min(1.0, intensity))
430444
let clampedDuration = max(0.01, min(0.12, Double(durationMs) / 1000.0))
445+
if !loggedHapticsCapability {
446+
if #available(iOS 13.0, *) {
447+
sendDiagnostic(
448+
"iOS 触感能力: supportsHaptics=\(CHHapticEngine.capabilitiesForHardware().supportsHaptics), supportsAudio=\(CHHapticEngine.capabilitiesForHardware().supportsAudio)"
449+
)
450+
} else {
451+
sendDiagnostic("iOS 触感能力: CoreHaptics unavailable below iOS 13")
452+
}
453+
loggedHapticsCapability = true
454+
}
431455

432456
if #available(iOS 13.0, *), CHHapticEngine.capabilitiesForHardware().supportsHaptics {
433457
do {
@@ -463,10 +487,18 @@ class AudioHapticsBridge {
463487
try player?.start(atTime: 0)
464488
return
465489
} catch {
490+
if !loggedCoreHapticsFailure {
491+
sendDiagnostic("CoreHaptics 播放失败,降级到 UIImpactFeedbackGenerator: \(error.localizedDescription)")
492+
loggedCoreHapticsFailure = true
493+
}
466494
// Fall back below.
467495
}
468496
}
469497

498+
if !loggedFallbackPulse {
499+
sendDiagnostic("使用 UIImpactFeedbackGenerator 触感降级路径")
500+
loggedFallbackPulse = true
501+
}
470502
let style: UIImpactFeedbackGenerator.FeedbackStyle =
471503
clampedIntensity > 0.72 ? .heavy : (clampedIntensity > 0.42 ? .medium : .light)
472504
impactGenerator = UIImpactFeedbackGenerator(style: style)

lib/src/screens/local_downloads_screen.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,15 +440,18 @@ class _LocalDownloadsScreenState extends ConsumerState<LocalDownloadsScreen>
440440

441441
void _openWorkDetail(int workId, DownloadTask task) async {
442442
_log.captureOutput(
443-
'[LocalDownloads] 打开作品详情: workId=$workId, hasMetadata=${task.workMetadata != null}');
443+
'[LocalDownloads] 打开作品详情: workId=$workId, task=${task.id}, '
444+
'file=${task.fileName}, hasMetadata=${task.workMetadata != null}');
444445

445446
final loadedMetadata = task.workMetadata ??
446447
await DownloadService.instance.getWorkMetadata(workId);
447448

448449
if (!mounted) return;
449450

450451
if (loadedMetadata == null) {
451-
_log.captureOutput('[LocalDownloads] 错误:任务没有元数据');
452+
_log.captureOutput(
453+
'[LocalDownloads] 错误:任务没有元数据,磁盘恢复也失败: workId=$workId, task=${task.id}',
454+
);
452455
_showSnackBarSafe(
453456
SnackBar(
454457
content: Text(S.of(context).noWorkMetadataForOffline),
@@ -460,6 +463,11 @@ class _LocalDownloadsScreenState extends ConsumerState<LocalDownloadsScreen>
460463

461464
try {
462465
final metadata = _sanitizeMetadata(loadedMetadata);
466+
_log.captureOutput(
467+
'[LocalDownloads] 已获得离线元数据: workId=$workId, '
468+
'metadataId=${metadata['id']}, sourceId=${metadata['source_id']}, '
469+
'localDir=${metadata['localWorkDirName']}',
470+
);
463471
final work = Work.fromJson(metadata);
464472

465473
// 动态构建完整的本地路径

lib/src/services/audio_haptics_service.dart

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ class AudioHapticsService {
4545
String? _activeStreamSource;
4646
String? _activeStreamFinalSource;
4747
bool _activeStreamSourceIsGrowing = false;
48+
bool _loggedFirstStreamingChunk = false;
49+
bool _loggedNoEventChunk = false;
50+
bool _loggedFirstStreamingEventBatch = false;
51+
bool _platformPulseFailureLogged = false;
52+
bool _timerStartLogged = false;
4853

4954
bool get enabled => _enabled;
5055
bool get _supportsPlatform => Platform.isIOS || Platform.isAndroid;
@@ -61,9 +66,22 @@ class AudioHapticsService {
6166
required bool enabled,
6267
required double intensity,
6368
}) async {
69+
final previousEnabled = _enabled;
70+
final previousIntensity = _intensity;
6471
_enabled = enabled && _supportsPlatform;
6572
_intensity = intensity.clamp(0.2, 1.0);
6673

74+
if (previousEnabled != _enabled ||
75+
(previousIntensity - _intensity).abs() > 0.001 ||
76+
enabled != _enabled) {
77+
_hapticsLog.info(
78+
'设置更新: requested=$enabled, effective=$_enabled, '
79+
'supported=$_supportsPlatform, intensity=${_intensity.toStringAsFixed(2)}, '
80+
'platform=${Platform.operatingSystem}',
81+
tag: 'AudioHaptics',
82+
);
83+
}
84+
6785
if (!_enabled) {
6886
await stop(clearSource: false);
6987
return;
@@ -91,6 +109,7 @@ class AudioHapticsService {
91109
_activeStreamFinalSource = null;
92110
_activeStreamSourceIsGrowing = false;
93111
_streamingPatternGenerator = null;
112+
_resetDiagnosticsForNewAnalysis();
94113

95114
if (!_enabled) return;
96115

@@ -105,6 +124,11 @@ class AudioHapticsService {
105124
final generation = _analysisGeneration;
106125

107126
try {
127+
_hapticsLog.info(
128+
'开始完整文件分析: title="${track.title}", path=${_shortPath(path)}, '
129+
'file=${await _fileState(path)}',
130+
tag: 'AudioHaptics',
131+
);
108132
final raw = await _channel.invokeMethod<Map<dynamic, dynamic>>(
109133
'analyze',
110134
{
@@ -123,8 +147,10 @@ class AudioHapticsService {
123147
_nextEventIndex = _indexForPosition(
124148
_positionProvider?.call() ?? Duration.zero,
125149
);
126-
_hapticsLog.captureOutput(
127-
'[AudioHaptics] 已生成 ${_events.length} 个触感事件: ${track.title}',
150+
_hapticsLog.info(
151+
'完整分析完成: title="${track.title}", frames=${analysis.energies.length}, '
152+
'events=${_events.length}, startIndex=$_nextEventIndex',
153+
tag: 'AudioHaptics',
128154
);
129155
if (_playingProvider?.call() ?? false) {
130156
start();
@@ -150,6 +176,7 @@ class AudioHapticsService {
150176
_activeStreamSource = source;
151177
_activeStreamFinalSource = finalSource;
152178
_activeStreamSourceIsGrowing = growingFile;
179+
_resetDiagnosticsForNewAnalysis();
153180
_streamingPatternGenerator = StreamingAudioHapticPatternGenerator(
154181
frameMs: AudioHapticPatternGenerator.defaultFrameMs,
155182
userIntensity: _intensity,
@@ -163,6 +190,14 @@ class AudioHapticsService {
163190
final method =
164191
growingFile ? 'startGrowingFileAnalysis' : 'startFileStreamAnalysis';
165192
try {
193+
_hapticsLog.info(
194+
'准备流式分析: title="${track.title}", method=$method, '
195+
'source=${_shortPath(source)}, sourceFile=${await _fileState(source)}, '
196+
'finalSource=${_shortPath(finalSource)}, '
197+
'finalFile=${finalSource == null ? 'none' : await _fileState(finalSource)}, '
198+
'start=${startPosition.inMilliseconds}ms',
199+
tag: 'AudioHaptics',
200+
);
166201
await _channel.invokeMethod<void>(method, {
167202
'path': source,
168203
if (finalSource != null) 'finalPath': finalSource,
@@ -172,8 +207,9 @@ class AudioHapticsService {
172207
'analysisToken': generation,
173208
});
174209
if (generation == _analysisGeneration) {
175-
_hapticsLog.captureOutput(
176-
'[AudioHaptics] 已启动流式分析: ${track.title}',
210+
_hapticsLog.info(
211+
'已启动流式分析: title="${track.title}", token=$generation',
212+
tag: 'AudioHaptics',
177213
);
178214
}
179215
} catch (e) {
@@ -188,6 +224,14 @@ class AudioHapticsService {
188224
if (!_enabled || _events.isEmpty) return;
189225
if (_timer?.isActive ?? false) return;
190226
_timer?.cancel();
227+
if (!_timerStartLogged) {
228+
_hapticsLog.info(
229+
'触感定时器启动: events=${_events.length}, '
230+
'position=${(_positionProvider?.call() ?? Duration.zero).inMilliseconds}ms',
231+
tag: 'AudioHaptics',
232+
);
233+
_timerStartLogged = true;
234+
}
191235
_timer = Timer.periodic(const Duration(milliseconds: 24), (_) {
192236
unawaited(_tick());
193237
});
@@ -206,6 +250,7 @@ class AudioHapticsService {
206250
_nextEventIndex = 0;
207251
_analysisGeneration++;
208252
_streamingPatternGenerator = null;
253+
_resetDiagnosticsForNewAnalysis();
209254
if (clearSource) {
210255
_activeStreamSource = null;
211256
_activeStreamFinalSource = null;
@@ -261,7 +306,14 @@ class AudioHapticsService {
261306
'intensity': event.intensity,
262307
'durationMs': event.durationMs,
263308
});
264-
} catch (_) {
309+
} catch (e) {
310+
if (!_platformPulseFailureLogged) {
311+
_hapticsLog.warning(
312+
'平台触感脉冲调用失败,后续同一分析周期内不再重复记录: $e',
313+
tag: 'AudioHaptics',
314+
);
315+
_platformPulseFailureLogged = true;
316+
}
265317
// Platform haptics are best-effort; never interrupt audio playback.
266318
}
267319
}
@@ -281,6 +333,12 @@ class AudioHapticsService {
281333
'[AudioHaptics] 流式分析失败: ${_analysisMessage(call.arguments)}',
282334
);
283335
return null;
336+
case 'diagnostic':
337+
_hapticsLog.info(
338+
_analysisMessage(call.arguments),
339+
tag: 'AudioHaptics',
340+
);
341+
return null;
284342
default:
285343
throw MissingPluginException(
286344
'Unknown audio haptics method: ${call.method}');
@@ -296,11 +354,40 @@ class AudioHapticsService {
296354
if (generator == null) return;
297355
generator.userIntensity = _intensity;
298356

357+
if (!_loggedFirstStreamingChunk) {
358+
_hapticsLog.info(
359+
'收到首个分析块: startFrame=${analysis.startFrame}, '
360+
'frameMs=${analysis.frameMs}, energies=${analysis.energies.length}, '
361+
'energyRange=${_energyRange(analysis.energies)}',
362+
tag: 'AudioHaptics',
363+
);
364+
_loggedFirstStreamingChunk = true;
365+
}
366+
299367
final newEvents = generator.append(
300368
startFrame: analysis.startFrame,
301369
energies: analysis.energies,
302370
);
303-
if (newEvents.isEmpty) return;
371+
if (newEvents.isEmpty) {
372+
if (!_loggedNoEventChunk) {
373+
_hapticsLog.info(
374+
'分析块暂未产生触感事件,可能是音量/动态不足: '
375+
'startFrame=${analysis.startFrame}, energies=${analysis.energies.length}',
376+
tag: 'AudioHaptics',
377+
);
378+
_loggedNoEventChunk = true;
379+
}
380+
return;
381+
}
382+
383+
if (!_loggedFirstStreamingEventBatch) {
384+
_hapticsLog.info(
385+
'生成首批流式触感事件: count=${newEvents.length}, '
386+
'first=${newEvents.first.timeMs}ms, last=${newEvents.last.timeMs}ms',
387+
tag: 'AudioHaptics',
388+
);
389+
_loggedFirstStreamingEventBatch = true;
390+
}
304391

305392
_events = [..._events, ...newEvents];
306393
if (_playingProvider?.call() ?? false) {
@@ -348,6 +435,42 @@ class AudioHapticsService {
348435
return null;
349436
}
350437

438+
void _resetDiagnosticsForNewAnalysis() {
439+
_loggedFirstStreamingChunk = false;
440+
_loggedNoEventChunk = false;
441+
_loggedFirstStreamingEventBatch = false;
442+
_platformPulseFailureLogged = false;
443+
_timerStartLogged = false;
444+
}
445+
446+
String _shortPath(String? path) {
447+
if (path == null || path.isEmpty) return 'none';
448+
if (path.length <= 96) return path;
449+
return '...${path.substring(path.length - 96)}';
450+
}
451+
452+
Future<String> _fileState(String path) async {
453+
try {
454+
final file = File(path);
455+
if (!await file.exists()) return 'missing';
456+
final length = await file.length();
457+
return 'exists:${length}B';
458+
} catch (e) {
459+
return 'error:$e';
460+
}
461+
}
462+
463+
String _energyRange(List<double> energies) {
464+
if (energies.isEmpty) return 'empty';
465+
var minValue = energies.first;
466+
var maxValue = energies.first;
467+
for (final energy in energies.skip(1)) {
468+
if (energy < minValue) minValue = energy;
469+
if (energy > maxValue) maxValue = energy;
470+
}
471+
return '${minValue.toStringAsFixed(3)}-${maxValue.toStringAsFixed(3)}';
472+
}
473+
351474
Future<void> _stopPlatformHaptics() async {
352475
if (!_supportsPlatform) return;
353476
try {

0 commit comments

Comments
 (0)