mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-30 14:20:15 +08:00
switch live stream/format/codec/url support
Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
@@ -102,7 +102,11 @@ abstract final class LiveHttp {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
return Success(RoomPlayInfoData.fromJson(res.data['data']));
|
try {
|
||||||
|
return Success(RoomPlayInfoData.fromJson(res.data['data']));
|
||||||
|
} catch (e) {
|
||||||
|
return Error(e.toString());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Error(res.data['message']);
|
return Error(res.data['message']);
|
||||||
}
|
}
|
||||||
@@ -163,7 +167,11 @@ abstract final class LiveHttp {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
return Success(LiveDmInfoData.fromJson(res.data['data']));
|
try {
|
||||||
|
return Success(LiveDmInfoData.fromJson(res.data['data']));
|
||||||
|
} catch (e) {
|
||||||
|
return Error(e.toString());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Error(res.data['message']);
|
return Error(res.data['message']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import 'package:PiliPlus/models_new/live/live_dm_info/host_list.dart';
|
import 'package:PiliPlus/models_new/live/live_dm_info/host_list.dart';
|
||||||
|
|
||||||
class LiveDmInfoData {
|
class LiveDmInfoData {
|
||||||
String? token;
|
String token;
|
||||||
List<HostList>? hostList;
|
List<HostList> hostList;
|
||||||
|
|
||||||
LiveDmInfoData({
|
LiveDmInfoData({
|
||||||
this.token,
|
required this.token,
|
||||||
this.hostList,
|
required this.hostList,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory LiveDmInfoData.fromJson(Map<String, dynamic> json) => LiveDmInfoData(
|
factory LiveDmInfoData.fromJson(Map<String, dynamic> json) => LiveDmInfoData(
|
||||||
token: json['token'] as String?,
|
token: json['token'] as String,
|
||||||
hostList: (json['host_list'] as List<dynamic>?)
|
hostList: (json['host_list'] as List<dynamic>)
|
||||||
?.map((e) => HostList.fromJson(e as Map<String, dynamic>))
|
.map((e) => HostList.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,27 @@ import 'package:PiliPlus/models_new/live/live_room_play_info/url_info.dart';
|
|||||||
import 'package:PiliPlus/utils/extension/iterable_ext.dart';
|
import 'package:PiliPlus/utils/extension/iterable_ext.dart';
|
||||||
|
|
||||||
class CodecItem {
|
class CodecItem {
|
||||||
int? currentQn;
|
String? codecName;
|
||||||
List<int>? acceptQn;
|
int currentQn;
|
||||||
String? baseUrl;
|
List<int> acceptQn;
|
||||||
List<UrlInfo>? urlInfo;
|
String baseUrl;
|
||||||
|
List<UrlInfo> urlInfo;
|
||||||
|
|
||||||
CodecItem({
|
CodecItem({
|
||||||
this.currentQn,
|
this.codecName,
|
||||||
this.acceptQn,
|
required this.currentQn,
|
||||||
this.baseUrl,
|
required this.acceptQn,
|
||||||
this.urlInfo,
|
required this.baseUrl,
|
||||||
|
required this.urlInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory CodecItem.fromJson(Map<String, dynamic> json) => CodecItem(
|
factory CodecItem.fromJson(Map<String, dynamic> json) => CodecItem(
|
||||||
currentQn: json['current_qn'] as int?,
|
codecName: json['codec_name'],
|
||||||
acceptQn: (json['accept_qn'] as List?)?.fromCast(),
|
currentQn: json['current_qn'] as int,
|
||||||
baseUrl: json['base_url'] as String?,
|
acceptQn: (json['accept_qn'] as List).fromCast(),
|
||||||
urlInfo: (json['url_info'] as List<dynamic>?)
|
baseUrl: json['base_url'] as String,
|
||||||
?.map((e) => UrlInfo.fromJson(e as Map<String, dynamic>))
|
urlInfo: (json['url_info'] as List<dynamic>)
|
||||||
|
.map((e) => UrlInfo.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart';
|
import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart';
|
||||||
|
|
||||||
class Format {
|
class Format {
|
||||||
List<CodecItem>? codec;
|
String? formatName;
|
||||||
|
List<CodecItem> codec;
|
||||||
|
|
||||||
Format({this.codec});
|
Format({
|
||||||
|
this.formatName,
|
||||||
|
required this.codec,
|
||||||
|
});
|
||||||
|
|
||||||
factory Format.fromJson(Map<String, dynamic> json) => Format(
|
factory Format.fromJson(Map<String, dynamic> json) => Format(
|
||||||
codec: (json['codec'] as List<dynamic>?)
|
formatName: json['format_name'],
|
||||||
?.map((e) => CodecItem.fromJson(e as Map<String, dynamic>))
|
codec: (json['codec'] as List<dynamic>)
|
||||||
|
.map((e) => CodecItem.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import 'package:PiliPlus/models_new/live/live_room_play_info/stream.dart';
|
import 'package:PiliPlus/models_new/live/live_room_play_info/stream.dart';
|
||||||
|
|
||||||
class Playurl {
|
class Playurl {
|
||||||
List<Stream>? stream;
|
List<Stream> stream;
|
||||||
|
|
||||||
Playurl({
|
Playurl({
|
||||||
this.stream,
|
required this.stream,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Playurl.fromJson(Map<String, dynamic> json) => Playurl(
|
factory Playurl.fromJson(Map<String, dynamic> json) => Playurl(
|
||||||
stream: (json['stream'] as List<dynamic>?)
|
stream: (json['stream'] as List<dynamic>)
|
||||||
?.map((e) => Stream.fromJson(e as Map<String, dynamic>))
|
.map((e) => Stream.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import 'package:PiliPlus/models_new/live/live_room_play_info/format.dart';
|
import 'package:PiliPlus/models_new/live/live_room_play_info/format.dart';
|
||||||
|
|
||||||
class Stream {
|
class Stream {
|
||||||
List<Format>? format;
|
String? protocolName;
|
||||||
|
List<Format> format;
|
||||||
|
|
||||||
Stream({this.format});
|
Stream({this.protocolName, required this.format});
|
||||||
|
|
||||||
factory Stream.fromJson(Map<String, dynamic> json) => Stream(
|
factory Stream.fromJson(Map<String, dynamic> json) => Stream(
|
||||||
format: (json['format'] as List<dynamic>?)
|
protocolName: json['protocol_name'],
|
||||||
?.map((e) => Format.fromJson(e as Map<String, dynamic>))
|
format: (json['format'] as List<dynamic>)
|
||||||
|
.map((e) => Format.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
class UrlInfo {
|
class UrlInfo {
|
||||||
String? host;
|
String host;
|
||||||
String? extra;
|
String extra;
|
||||||
|
|
||||||
UrlInfo({this.host, this.extra});
|
UrlInfo({required this.host, required this.extra});
|
||||||
|
|
||||||
factory UrlInfo.fromJson(Map<String, dynamic> json) => UrlInfo(
|
factory UrlInfo.fromJson(Map<String, dynamic> json) => UrlInfo(
|
||||||
host: json['host'] as String?,
|
host: json['host'] as String,
|
||||||
extra: json['extra'] as String?,
|
extra: json['extra'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:PiliPlus/models_new/live/live_dm_info/data.dart';
|
|||||||
import 'package:PiliPlus/models_new/live/live_medal_wall/uinfo_medal.dart';
|
import 'package:PiliPlus/models_new/live/live_medal_wall/uinfo_medal.dart';
|
||||||
import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart';
|
import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart';
|
||||||
import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart';
|
import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart';
|
||||||
|
import 'package:PiliPlus/models_new/live/live_room_play_info/stream.dart';
|
||||||
import 'package:PiliPlus/models_new/live/live_superchat/item.dart';
|
import 'package:PiliPlus/models_new/live/live_superchat/item.dart';
|
||||||
import 'package:PiliPlus/pages/common/publish/publish_route.dart';
|
import 'package:PiliPlus/pages/common/publish/publish_route.dart';
|
||||||
import 'package:PiliPlus/pages/danmaku/danmaku_model.dart';
|
import 'package:PiliPlus/pages/danmaku/danmaku_model.dart';
|
||||||
@@ -195,38 +196,68 @@ class LiveRoomController extends GetxController {
|
|||||||
_showDialog('当前直播间未开播');
|
_showDialog('当前直播间未开播');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (response.playurlInfo?.playurl == null) {
|
final playurl = response.playurlInfo?.playurl;
|
||||||
|
if (playurl == null) {
|
||||||
_showDialog('无法获取播放地址');
|
_showDialog('无法获取播放地址');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ruid = response.uid;
|
ruid = response.uid;
|
||||||
if (response.roomId != null) {
|
if (response.roomId case final roomId?) {
|
||||||
roomId = response.roomId!;
|
this.roomId = roomId;
|
||||||
}
|
}
|
||||||
liveTime.value = response.liveTime;
|
liveTime.value = response.liveTime;
|
||||||
startLiveTimer();
|
startLiveTimer();
|
||||||
isPortrait.value = response.isPortrait ?? false;
|
isPortrait.value = response.isPortrait ?? false;
|
||||||
List<CodecItem> codec =
|
stream = playurl.stream;
|
||||||
response.playurlInfo!.playurl!.stream!.first.format!.first.codec!;
|
await initLiveUrl(
|
||||||
CodecItem item = codec.first;
|
streamIndex: streamIndex,
|
||||||
// 以服务端返回的码率为准
|
formatIndex: formatIndex,
|
||||||
currentQn = item.currentQn!;
|
codecIndex: codecIndex,
|
||||||
acceptQnList = item.acceptQn!.map((e) {
|
liveUrlIndex: liveUrlIndex,
|
||||||
return (
|
);
|
||||||
code: e,
|
|
||||||
desc: LiveQuality.fromCode(e)?.desc ?? e.toString(),
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
currentQnDesc.value =
|
|
||||||
LiveQuality.fromCode(currentQn)?.desc ?? currentQn.toString();
|
|
||||||
videoUrl = VideoUtils.getLiveCdnUrl(item);
|
|
||||||
await playerInit(autoFullScreenFlag: autoFullScreenFlag);
|
|
||||||
isLoaded.value = true;
|
isLoaded.value = true;
|
||||||
} else {
|
} else {
|
||||||
_showDialog(res.toString());
|
_showDialog(res.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late List<Stream> stream;
|
||||||
|
int streamIndex = 0;
|
||||||
|
int formatIndex = 0;
|
||||||
|
int codecIndex = 0;
|
||||||
|
int liveUrlIndex = 0;
|
||||||
|
|
||||||
|
Future<void>? initLiveUrl({
|
||||||
|
int streamIndex = 0,
|
||||||
|
int formatIndex = 0,
|
||||||
|
int codecIndex = 0,
|
||||||
|
int liveUrlIndex = 0,
|
||||||
|
}) {
|
||||||
|
this.streamIndex = streamIndex;
|
||||||
|
this.formatIndex = formatIndex;
|
||||||
|
this.codecIndex = codecIndex;
|
||||||
|
this.liveUrlIndex = liveUrlIndex;
|
||||||
|
|
||||||
|
final CodecItem item = stream
|
||||||
|
.getOrFirst(streamIndex)
|
||||||
|
.format
|
||||||
|
.getOrFirst(formatIndex)
|
||||||
|
.codec
|
||||||
|
.getOrFirst(codecIndex);
|
||||||
|
// 以服务端返回的码率为准
|
||||||
|
currentQn = item.currentQn;
|
||||||
|
acceptQnList = item.acceptQn.map((e) {
|
||||||
|
return (
|
||||||
|
code: e,
|
||||||
|
desc: LiveQuality.fromCode(e)?.desc ?? e.toString(),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
currentQnDesc.value =
|
||||||
|
LiveQuality.fromCode(currentQn)?.desc ?? currentQn.toString();
|
||||||
|
videoUrl = VideoUtils.getLiveCdnUrl(item, index: liveUrlIndex);
|
||||||
|
return playerInit();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> queryLiveInfoH5() async {
|
Future<void> queryLiveInfoH5() async {
|
||||||
final res = await LiveHttp.liveRoomInfoH5(roomId: roomId);
|
final res = await LiveHttp.liveRoomInfoH5(roomId: roomId);
|
||||||
if (res case Success(:final response)) {
|
if (res case Success(:final response)) {
|
||||||
@@ -414,10 +445,10 @@ class LiveRoomController extends GetxController {
|
|||||||
}
|
}
|
||||||
_msgStream =
|
_msgStream =
|
||||||
LiveMessageStream(
|
LiveMessageStream(
|
||||||
streamToken: info.token!,
|
streamToken: info.token,
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
uid: Accounts.heartbeat.mid,
|
uid: Accounts.heartbeat.mid,
|
||||||
servers: info.hostList!
|
servers: info.hostList
|
||||||
.map((host) => 'wss://${host.host}:${host.wssPort}/sub')
|
.map((host) => 'wss://${host.host}:${host.wssPort}/sub')
|
||||||
.toList(),
|
.toList(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io' show Platform;
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:PiliPlus/common/style.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/flutter/draggable_scrollable_sheet.dart';
|
||||||
import 'package:PiliPlus/common/widgets/marquee.dart';
|
import 'package:PiliPlus/common/widgets/marquee.dart';
|
||||||
|
import 'package:PiliPlus/models/common/video/live_quality.dart';
|
||||||
import 'package:PiliPlus/pages/live_room/controller.dart';
|
import 'package:PiliPlus/pages/live_room/controller.dart';
|
||||||
import 'package:PiliPlus/pages/setting/models/play_settings.dart'
|
import 'package:PiliPlus/pages/setting/models/play_settings.dart'
|
||||||
show showPlayerVolumeDialog;
|
show showPlayerVolumeDialog;
|
||||||
@@ -10,6 +14,8 @@ import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart';
|
|||||||
import 'package:PiliPlus/services/shutdown_timer_service.dart'
|
import 'package:PiliPlus/services/shutdown_timer_service.dart'
|
||||||
show shutdownTimerService;
|
show shutdownTimerService;
|
||||||
import 'package:PiliPlus/utils/android/bindings.g.dart';
|
import 'package:PiliPlus/utils/android/bindings.g.dart';
|
||||||
|
import 'package:PiliPlus/utils/extension/context_ext.dart';
|
||||||
|
import 'package:PiliPlus/utils/extension/size_ext.dart';
|
||||||
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
import 'package:PiliPlus/utils/extension/string_ext.dart';
|
||||||
import 'package:PiliPlus/utils/platform_utils.dart';
|
import 'package:PiliPlus/utils/platform_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -237,28 +243,39 @@ class _LiveHeaderControlState extends State<LiveHeaderControl>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (plPlayerController.videoPlayerController case final player?)
|
if (plPlayerController.videoPlayerController case final player?)
|
||||||
if (PlatformUtils.isMobile)
|
SizedBox.square(
|
||||||
SizedBox.square(
|
dimension: 30,
|
||||||
dimension: 30,
|
child: PopupMenuButton(
|
||||||
child: PopupMenuButton(
|
iconSize: 18,
|
||||||
iconSize: 18,
|
padding: .zero,
|
||||||
padding: .zero,
|
iconColor: Colors.white,
|
||||||
iconColor: Colors.white,
|
itemBuilder: (context) => [
|
||||||
itemBuilder: (context) => [
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
height: 35,
|
||||||
height: 35,
|
onTap: _showLiveStreamDialog,
|
||||||
child: const Row(
|
child: const Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.info_outline, size: 17),
|
Icon(Icons.alt_route, size: 17),
|
||||||
Text('播放信息', style: TextStyle(fontSize: 14)),
|
Text('切换路线', style: TextStyle(fontSize: 14)),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
onTap: () => HeaderControlState.showPlayerInfo(
|
|
||||||
context,
|
|
||||||
player: player,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
height: 35,
|
||||||
|
child: const Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.info_outline, size: 17),
|
||||||
|
Text('播放信息', style: TextStyle(fontSize: 14)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => HeaderControlState.showPlayerInfo(
|
||||||
|
context,
|
||||||
|
player: player,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (PlatformUtils.isMobile)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
height: 35,
|
height: 35,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -277,23 +294,172 @@ class _LiveHeaderControlState extends State<LiveHeaderControl>
|
|||||||
onChanged: player.setVolume,
|
onChanged: player.setVolume,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ComBtn(
|
|
||||||
height: 30,
|
|
||||||
tooltip: '播放信息',
|
|
||||||
onTap: () =>
|
|
||||||
HeaderControlState.showPlayerInfo(context, player: player),
|
|
||||||
icon: const Icon(
|
|
||||||
size: 18,
|
|
||||||
Icons.info_outline,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showLiveStreamDialog() {
|
||||||
|
final controller = widget.liveController;
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
useSafeArea: true,
|
||||||
|
clipBehavior: .hardEdge,
|
||||||
|
isScrollControlled: true,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: math.min(640, context.mediaQueryShortestSide),
|
||||||
|
),
|
||||||
|
builder: (context) {
|
||||||
|
final maxChildSize =
|
||||||
|
PlatformUtils.isMobile && !context.mediaQuerySize.isPortrait
|
||||||
|
? 1.0
|
||||||
|
: 0.7;
|
||||||
|
return DynDraggableScrollableSheet(
|
||||||
|
minChildSize: 0,
|
||||||
|
maxChildSize: maxChildSize,
|
||||||
|
snap: true,
|
||||||
|
expand: false,
|
||||||
|
snapSizes: [maxChildSize],
|
||||||
|
initialChildSize: maxChildSize,
|
||||||
|
builder: (context, scrollController) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final secondary = theme.colorScheme.secondary;
|
||||||
|
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
|
||||||
|
final currStyle = TextStyle(fontSize: 14, color: secondary);
|
||||||
|
return Theme(
|
||||||
|
data: theme.copyWith(dividerColor: Colors.transparent),
|
||||||
|
child: ListView(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: .only(
|
||||||
|
bottom: MediaQuery.viewPaddingOf(context).bottom + 100,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: Get.back,
|
||||||
|
borderRadius: Style.bottomSheetRadius,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 35,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 32,
|
||||||
|
height: 3,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
borderRadius: const .all(.circular(1.5)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...controller.stream.indexed.map((stream) {
|
||||||
|
final isCurrStream = stream.$1 == controller.streamIndex;
|
||||||
|
final streamColor = isCurrStream
|
||||||
|
? secondary
|
||||||
|
: onSurfaceVariant;
|
||||||
|
return _ExpansionTile(
|
||||||
|
initiallyExpanded: isCurrStream,
|
||||||
|
iconColor: streamColor,
|
||||||
|
collapsedIconColor: streamColor,
|
||||||
|
title: Text(
|
||||||
|
stream.$2.protocolName ?? stream.$1.toString(),
|
||||||
|
style: isCurrStream
|
||||||
|
? currStyle
|
||||||
|
: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
children: stream.$2.format.indexed.map((format) {
|
||||||
|
final isCurrFormat =
|
||||||
|
isCurrStream && format.$1 == controller.formatIndex;
|
||||||
|
final formatColor = isCurrFormat
|
||||||
|
? secondary
|
||||||
|
: onSurfaceVariant;
|
||||||
|
return _ExpansionTile(
|
||||||
|
initiallyExpanded: isCurrFormat,
|
||||||
|
iconColor: formatColor,
|
||||||
|
collapsedIconColor: formatColor,
|
||||||
|
title: Text(
|
||||||
|
format.$2.formatName ?? format.$1.toString(),
|
||||||
|
style: isCurrFormat
|
||||||
|
? currStyle
|
||||||
|
: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
children: format.$2.codec.indexed.map((codec) {
|
||||||
|
final e = codec.$2;
|
||||||
|
final isCurrCodec =
|
||||||
|
isCurrFormat &&
|
||||||
|
codec.$1 == controller.codecIndex;
|
||||||
|
final codecColor = isCurrCodec
|
||||||
|
? secondary
|
||||||
|
: onSurfaceVariant;
|
||||||
|
return _ExpansionTile(
|
||||||
|
initiallyExpanded: isCurrCodec,
|
||||||
|
iconColor: codecColor,
|
||||||
|
collapsedIconColor: codecColor,
|
||||||
|
title: Text(
|
||||||
|
'${e.codecName ?? codec.$1.toString()} (${LiveQuality.fromCode(e.currentQn)?.desc ?? e.currentQn})',
|
||||||
|
style: isCurrCodec
|
||||||
|
? currStyle
|
||||||
|
: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
children: e.urlInfo.indexed.map((url) {
|
||||||
|
final isCurrUrl =
|
||||||
|
(isCurrCodec &&
|
||||||
|
url.$1 == controller.liveUrlIndex);
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
'${url.$2.host}${e.baseUrl}...',
|
||||||
|
style: isCurrUrl
|
||||||
|
? const TextStyle(fontSize: 14)
|
||||||
|
: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selected: isCurrUrl,
|
||||||
|
onTap: isCurrUrl
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
Get.back();
|
||||||
|
controller.initLiveUrl(
|
||||||
|
streamIndex: stream.$1,
|
||||||
|
formatIndex: format.$1,
|
||||||
|
codecIndex: codec.$1,
|
||||||
|
liveUrlIndex: url.$1,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpansionTile extends ExpansionTile {
|
||||||
|
const _ExpansionTile({
|
||||||
|
required super.title,
|
||||||
|
// ignore: unused_element_parameter
|
||||||
|
super.dense = true,
|
||||||
|
// ignore: unused_element_parameter
|
||||||
|
super.controlAffinity = .leading,
|
||||||
|
// ignore: unused_element_parameter
|
||||||
|
super.childrenPadding = const .only(left: 20),
|
||||||
|
super.initiallyExpanded,
|
||||||
|
super.iconColor,
|
||||||
|
super.collapsedIconColor,
|
||||||
|
super.children,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,4 +64,8 @@ extension ListExt<T> on List<T> {
|
|||||||
if (index < 0 || index >= length) return null;
|
if (index < 0 || index >= length) return null;
|
||||||
return this[index];
|
return this[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
T getOrFirst(int index) {
|
||||||
|
return getOrNull(index) ?? first;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:PiliPlus/models/common/video/cdn_type.dart';
|
import 'package:PiliPlus/models/common/video/cdn_type.dart';
|
||||||
import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart';
|
import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart';
|
||||||
|
import 'package:PiliPlus/utils/extension/iterable_ext.dart';
|
||||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||||
import 'package:flutter/foundation.dart' show kDebugMode, debugPrint;
|
import 'package:flutter/foundation.dart' show kDebugMode, debugPrint;
|
||||||
|
|
||||||
@@ -88,9 +89,8 @@ abstract final class VideoUtils {
|
|||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getLiveCdnUrl(CodecItem e) {
|
static String getLiveCdnUrl(CodecItem e, {int index = 0}) {
|
||||||
return (liveCdnUrl ?? e.urlInfo!.first.host!) +
|
final urlInfo = e.urlInfo.getOrFirst(index);
|
||||||
e.baseUrl! +
|
return (liveCdnUrl ?? urlInfo.host) + e.baseUrl + urlInfo.extra;
|
||||||
e.urlInfo!.first.extra!;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user