import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show ByteData, Uint8List, rootBundle; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:star_lock/app_settings/app_colors.dart'; import 'package:star_lock/talk/startChart/handle/other/talk_data_repository.dart'; import 'package:star_lock/talk/startChart/proto/talk_data.pbserver.dart'; import 'package:star_lock/tools/titleAppBar.dart'; import 'package:webview_flutter/webview_flutter.dart'; class H264WebView extends StatefulWidget { @override _H264WebViewState createState() => _H264WebViewState(); } class _H264WebViewState extends State { late final WebViewController _controller; Timer? timer; Timer? _sendTimer; // 私有缓冲区,外部无法直接访问 final List _buffer = []; // 发送数据至html文件间隔时间 final int sendDataToHtmlIntervalTime = 820; // 通话数据流的单例流数据处理类 final TalkDataRepository talkDataRepository = TalkDataRepository.instance; @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..enableZoom(false) ..addJavaScriptChannel( 'Flutter', onMessageReceived: (message) { print("来自 HTML 的消息: ${message.message}"); }, ); // 加载本地 HTML _loadLocalHtml(); simulateStreamFromAsset(); _sendFramesToHtml(); } void simulateStreamFromAsset() async { // 读取 assets 文件 final ByteData data = await rootBundle.load('assets/talk.h264'); final List byteData = data.buffer.asUint8List(); int current = 0; int start = 0; int end = 0; final List chunks = extractChunks(byteData); // 定时器控制发送数据块的节奏 timer ??= Timer.periodic(Duration(milliseconds: 10), (timer) { if (current >= chunks.length) { print('数据已经发完,重新进行发送'); start = 0; end = 0; current = 0; timer.cancel(); return; } // 提取 NALU 边界并生成 chunks end = chunks[current]; current++; List frameData = byteData.sublist(start, end); if (frameData.length == 0) timer.cancel(); talkDataRepository.addTalkData(TalkData(contentType: TalkData_ContentTypeE.H264,content: frameData)); start = end; }); } void _sendFramesToHtml() async { // 接收到流数据,保存到缓冲区 talkDataRepository.talkDataStream.listen((TalkData event) async { _buffer.addAll(event.content); }); // 缓冲800ms的数据,定时发送 _sendTimer ??= Timer.periodic( Duration(milliseconds: sendDataToHtmlIntervalTime), (timer) async { // 发送累积的数据 if (_buffer.isNotEmpty) { await _sendBufferedData(_buffer); _buffer.clear(); // 清空缓冲区 } }); } // 提取 NALU 边界并生成 chunks List extractChunks(List byteData) { int i = 0; int length = byteData.length; int naluCount = 0; int value; int state = 0; int lastIndex = 0; List result = []; const minNaluPerChunk = 22; // 每个数据块包含的最小NALU数量 while (i < length) { value = byteData[i++]; // finding 3 or 4-byte start codes (00 00 01 OR 00 00 00 01) switch (state) { case 0: if (value == 0) { state = 1; } break; case 1: if (value == 0) { state = 2; } else { state = 0; } break; case 2: case 3: if (value == 0) { state = 3; } else if (value == 1 && i < length) { if (lastIndex > 0) { naluCount++; } if (naluCount >= minNaluPerChunk) { result.add(lastIndex - state - 1); naluCount = 0; } state = 0; lastIndex = i; } else { state = 0; } break; default: break; } } if (naluCount > 0) { result.add(lastIndex); } return result; } /// 加载html文件 Future _loadLocalHtml() async { final String fileHtmlContent = await rootBundle.loadString('assets/html/h264.html'); _controller.loadHtmlString(fileHtmlContent); } // 发送数据给js处理 _sendBufferedData(List buffer) async { String jsCode = "feedDataFromFlutter(${buffer});"; await _controller.runJavaScript(jsCode); } @override Widget build(BuildContext context) { return WebViewWidget(controller: _controller); } @override void dispose() { timer?.cancel(); timer = null; _sendTimer?.cancel(); timer = null; // talkDataRepository.dispose(); super.dispose(); } }