feat: 直播间播放

This commit is contained in:
guozhigq
2023-07-11 21:32:31 +08:00
parent 75d4e20d99
commit 828b5c39aa
9 changed files with 374 additions and 7 deletions

View File

@ -190,4 +190,10 @@ class Api {
// ?page=1&page_size=30&platform=web
static const String liveList =
'https://api.live.bilibili.com/xlive/web-interface/v1/second/getUserRecommend';
// 直播间详情
// cid roomId
// qn 80:流畅150:高清400:蓝光10000:原画20000:4K, 30000:杜比
static const String liveRoomInfo =
'https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo';
}

View File

@ -1,6 +1,7 @@
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/models/live/room_info.dart';
class LiveHttp {
static Future liveList(
@ -22,4 +23,27 @@ class LiveHttp {
};
}
}
static Future liveRoomInfo({roomId, qn}) async {
var res = await Request().get(Api.liveRoomInfo, data: {
'room_id': roomId,
'protocol': '0, 1',
'format': '0, 1, 2',
'codec': '0, 1',
'qn': qn,
'platform': 'web',
'ptype': 8,
'dolby': 5,
'panorama': 1,
});
if (res.data['code'] == 0) {
return {'status': true, 'data': RoomInfoModel.fromJson(res.data['data'])};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -50,7 +50,7 @@ class LiveItemModel {
Map? watchedShow;
LiveItemModel.fromJson(Map<String, dynamic> json) {
roomId = json['room_id'];
roomId = json['roomid'];
uid = json['uid'];
title = json['title'];
uname = json['uname'];

View File

@ -0,0 +1,155 @@
class RoomInfoModel {
RoomInfoModel({
this.roomId,
this.liveStatus,
this.liveTime,
this.playurlInfo,
});
int? roomId;
int? liveStatus;
int? liveTime;
PlayurlInfo? playurlInfo;
RoomInfoModel.fromJson(Map<String, dynamic> json) {
roomId = json['room_id'];
liveStatus = json['live_status'];
liveTime = json['live_time'];
playurlInfo = PlayurlInfo.fromJson(json['playurl_info']);
}
}
class PlayurlInfo {
PlayurlInfo({
this.playurl,
});
Playurl? playurl;
PlayurlInfo.fromJson(Map<String, dynamic> json) {
playurl = Playurl.fromJson(json['playurl']);
}
}
class Playurl {
Playurl({
this.cid,
this.gQnDesc,
this.stream,
});
int? cid;
List<GQnDesc>? gQnDesc;
List<Streams>? stream;
Playurl.fromJson(Map<String, dynamic> json) {
cid = json['cid'];
gQnDesc =
json['g_qn_desc'].map<GQnDesc>((e) => GQnDesc.fromJson(e)).toList();
stream = json['stream'].map<Streams>((e) => Streams.fromJson(e)).toList();
}
}
class GQnDesc {
GQnDesc({
this.qn,
this.desc,
this.hdrDesc,
this.attrDesc,
});
int? qn;
String? desc;
String? hdrDesc;
String? attrDesc;
GQnDesc.fromJson(Map<String, dynamic> json) {
qn = json['qn'];
desc = json['desc'];
hdrDesc = json['hedr_desc'];
attrDesc = json['attr_desc'];
}
}
class Streams {
Streams({
this.protocolName,
this.format,
});
String? protocolName;
List<FormatItem>? format;
Streams.fromJson(Map<String, dynamic> json) {
protocolName = json['protocol_name'];
format =
json['format'].map<FormatItem>((e) => FormatItem.fromJson(e)).toList();
}
}
class FormatItem {
FormatItem({
this.formatName,
this.codec,
});
String? formatName;
List<CodecItem>? codec;
FormatItem.fromJson(Map<String, dynamic> json) {
formatName = json['format_name'];
codec = json['codec'].map<CodecItem>((e) => CodecItem.fromJson(e)).toList();
}
}
class CodecItem {
CodecItem({
this.codecName,
this.currentQn,
this.acceptQn,
this.baseUrl,
this.urlInfo,
this.hdrQn,
this.dolbyType,
this.attrName,
});
String? codecName;
int? currentQn;
List? acceptQn;
String? baseUrl;
List<UrlInfoItem>? urlInfo;
String? hdrQn;
int? dolbyType;
String? attrName;
CodecItem.fromJson(Map<String, dynamic> json) {
codecName = json['codec_name'];
currentQn = json['current_qn'];
acceptQn = json['accept_qn'];
baseUrl = json['base_url'];
urlInfo = json['url_info']
.map<UrlInfoItem>((e) => UrlInfoItem.fromJson(e))
.toList();
hdrQn = json['hdr_n'];
dolbyType = json['dolby_type'];
attrName = json['attr_name'];
}
}
class UrlInfoItem {
UrlInfoItem({
this.host,
this.extra,
this.streamTtl,
});
String? host;
String? extra;
int? streamTtl;
UrlInfoItem.fromJson(Map<String, dynamic> json) {
host = json['host'];
extra = json['extra'];
streamTtl = json['stream_ttl'];
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
@ -22,7 +23,7 @@ class LiveCardV extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomId);
return Card(
elevation: 0,
elevation: 0.8,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
@ -42,13 +43,16 @@ class LiveCardV extends StatelessWidget {
child: InkWell(
onTap: () async {
await Future.delayed(const Duration(milliseconds: 200));
// Get.toNamed('/video?bvid=${liveItem.bvid}&cid=${liveItem.cid}',
// arguments: {'videoItem': liveItem, 'heroTag': heroTag});
Get.toNamed('/liveRoom?roomid=${liveItem.roomId}',
arguments: {'liveItem': liveItem, 'heroTag': heroTag});
},
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(StyleString.imgRadius),
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
@ -86,7 +90,7 @@ class LiveContent extends StatelessWidget {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 8, 6, 7),
padding: const EdgeInsets.fromLTRB(8, 8, 6, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -99,7 +103,7 @@ class LiveContent extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3),
const SizedBox(height: 4),
Row(
children: [
UpTag(),
@ -116,6 +120,7 @@ class LiveContent extends StatelessWidget {
)
],
),
const SizedBox(height: 2),
Row(
children: [
Text(

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_meedu_media_kit/meedu_player.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/room_info.dart';
class LiveRoomController extends GetxController {
String cover = '';
late int roomId;
var liveItem;
MeeduPlayerController meeduPlayerController = MeeduPlayerController(
colorTheme: Theme.of(Get.context!).colorScheme.primary,
pipEnabled: true,
controlsStyle: ControlsStyle.youtube,
enabledButtons: const EnabledButtons(pip: true),
);
@override
void onInit() {
super.onInit();
var args = Get.arguments['liveItem'];
liveItem = args;
print(liveItem.roomId);
roomId = liveItem.roomId!;
if (args.pic != null && args.pic != '') {
cover = args.cover;
}
queryLiveInfo();
}
playerInit(source) {
meeduPlayerController.setDataSource(
DataSource(
type: DataSourceType.network,
source: source,
httpHeaders: {
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15',
'referer': HttpString.baseUrl
},
),
autoplay: true,
);
}
Future queryLiveInfo() async {
var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: 10000);
if (res['status']) {
List<CodecItem> codec =
res['data'].playurlInfo.playurl.stream.first.format.first.codec;
CodecItem item = codec.first;
String videoUrl = (item.urlInfo?.first.host)! +
item.baseUrl! +
item.urlInfo!.first.extra!;
playerInit(videoUrl);
}
}
}

View File

@ -0,0 +1,4 @@
library liveroom;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:flutter_meedu_media_kit/meedu_player.dart';
import 'package:get/get.dart';
import 'dart:ui';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'controller.dart';
class LiveRoomPage extends StatefulWidget {
const LiveRoomPage({super.key});
@override
State<LiveRoomPage> createState() => _LiveRoomPageState();
}
class _LiveRoomPageState extends State<LiveRoomPage> {
final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
MeeduPlayerController? _meeduPlayerController;
@override
void initState() {
super.initState();
_meeduPlayerController = _liveRoomController.meeduPlayerController;
}
@override
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
return Scaffold(
primary: true,
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController.liveItem.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.liveItem.uname,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 3),
Text(_liveRoomController.liveItem.title,
style: const TextStyle(fontSize: 12)),
],
)
],
),
),
body: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
child: NetworkImgLayer(
type: 'emote',
src: _liveRoomController.cover,
width: Get.size.width,
height: videoHeight + 45,
),
),
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.background.withOpacity(0.1),
),
),
),
),
Scaffold(
backgroundColor: Colors.transparent,
body: Column(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: MeeduVideoPlayer(
controller: _meeduPlayerController!,
),
),
Container(
color: Theme.of(context).colorScheme.background,
height: 45,
padding: const EdgeInsets.only(left: 12, right: 12),
child: Row(children: [
Text(
_liveRoomController.liveItem.watchedShow['text_large']),
]),
),
],
),
)
],
),
);
}
}

View File

@ -9,6 +9,7 @@ import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/pages/liveRoom/view.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/searchResult/index.dart';
@ -59,5 +60,7 @@ class Routes {
GetPage(name: '/follow', page: () => const FollowPage()),
// 粉丝
GetPage(name: '/fan', page: () => const FansPage()),
// 直播详情
GetPage(name: '/liveRoom', page: () => const LiveRoomPage()),
];
}