138 lines
4.1 KiB
Dart
138 lines
4.1 KiB
Dart
import 'dart:convert';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart' show ByteData, Uint8List, rootBundle;
|
||
import 'package:webview_flutter/webview_flutter.dart';
|
||
|
||
class LocalHtmlPage extends StatefulWidget {
|
||
@override
|
||
_LocalHtmlPageState createState() => _LocalHtmlPageState();
|
||
}
|
||
|
||
class _LocalHtmlPageState extends State<LocalHtmlPage> {
|
||
late final WebViewController _controller;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_controller = WebViewController()
|
||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||
..enableZoom(false)
|
||
..addJavaScriptChannel(
|
||
'Flutter',
|
||
onMessageReceived: (message) {
|
||
print("来自 HTML 的消息: ${message.message}");
|
||
},
|
||
);
|
||
|
||
// 加载本地 HTML
|
||
_loadLocalHtml();
|
||
_sendFramesToHtml();
|
||
}
|
||
|
||
void _sendFramesToHtml() async {
|
||
// 读取 assets/demo.h264 文件
|
||
final ByteData data = await rootBundle.load('assets/talk.h264');
|
||
final List<int> byteData = data.buffer.asUint8List();
|
||
|
||
int offset = 0;
|
||
int frameSize = 0;
|
||
|
||
// 根据 H.264 数据的帧结构来逐帧读取并发送
|
||
while (offset < byteData.length) {
|
||
// 获取每一帧的大小
|
||
frameSize = getFrameSize(byteData, offset);
|
||
|
||
if (frameSize == 0) {
|
||
print("No more frames or error in frame size calculation.");
|
||
break; // 如果没有更多的帧,或者无法计算帧的大小,则退出循环
|
||
}
|
||
|
||
// 提取当前帧数据
|
||
List<int> frameData = byteData.sublist(offset, offset + frameSize);
|
||
|
||
// 将当前帧数据发送到 WebView 中的 feedDataFromFlutter 函数
|
||
String jsCode = "feedDataFromFlutter(${frameData});";
|
||
await _controller.runJavaScript(jsCode);
|
||
|
||
// 更新偏移量,继续发送下一个帧
|
||
offset += frameSize;
|
||
|
||
// 控制帧率,模拟每秒 22 帧播放(或者根据你的视频帧率调整)
|
||
await Future.delayed(Duration(milliseconds: (1000 / 22).toInt()));
|
||
}
|
||
}
|
||
|
||
int getFrameSize(List<int> data, int offset) {
|
||
// 查找第一个 Start Code (0x000001 或 0x00000001)
|
||
const List<int> startCode1 = [0, 0, 0, 1]; // Start Code: 0x000001
|
||
const List<int> startCode2 = [
|
||
0,
|
||
0,
|
||
1
|
||
]; // Start Code: 0x000001 (0x00000001 version)
|
||
|
||
// 查找下一个 Start Code 的位置
|
||
int startCodeStart = -1;
|
||
int startCodeEnd = -1;
|
||
for (int i = offset; i < data.length - 3; i++) {
|
||
// 判断是否匹配 Start Code (0x000001 或 0x00000001)
|
||
if (_matchesStartCode(data, i, startCode1)) {
|
||
startCodeStart = i;
|
||
startCodeEnd = i + startCode1.length;
|
||
break;
|
||
} else if (_matchesStartCode(data, i, startCode2)) {
|
||
startCodeStart = i;
|
||
startCodeEnd = i + startCode2.length;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果找不到有效的 Start Code,返回 0(结束循环)
|
||
if (startCodeStart == -1) return 0;
|
||
|
||
// 查找下一个 Start Code 的位置
|
||
int nextStartCodeStart = -1;
|
||
for (int i = startCodeEnd; i < data.length - 3; i++) {
|
||
if (_matchesStartCode(data, i, startCode1) ||
|
||
_matchesStartCode(data, i, startCode2)) {
|
||
nextStartCodeStart = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果找不到下一个 Start Code,说明当前是最后一个 NAL 单元
|
||
if (nextStartCodeStart == -1) {
|
||
return data.length - startCodeStart;
|
||
}
|
||
|
||
// 计算并返回当前 NAL 单元的大小
|
||
return nextStartCodeStart - startCodeStart;
|
||
}
|
||
|
||
// 检查数据是否匹配 Start Code
|
||
bool _matchesStartCode(List<int> data, int index, List<int> startCode) {
|
||
for (int i = 0; i < startCode.length; i++) {
|
||
if (data[index + i] != startCode[i]) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
Future<void> _loadLocalHtml() async {
|
||
final String fileHtmlContent =
|
||
await rootBundle.loadString('assets/html/h264.html');
|
||
_controller.loadHtmlString(fileHtmlContent);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: Text("加载本地 HTML"),
|
||
),
|
||
body: WebViewWidget(controller: _controller),
|
||
);
|
||
}
|
||
}
|