mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-08-03 09:10:13 +08:00
@@ -24,6 +24,8 @@ class Api {
|
||||
|
||||
static const String pugvUrl = '/pugv/player/web/playurl';
|
||||
|
||||
static const String tvPlayUrl = '/x/tv/playurl';
|
||||
|
||||
// 字幕
|
||||
// aid, cid
|
||||
static const String playInfo = '/x/player/wbi/v2';
|
||||
|
||||
@@ -204,11 +204,7 @@ abstract final class LiveHttp {
|
||||
'statistics': Constants.statisticsApp,
|
||||
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
};
|
||||
AppSign.appSign(
|
||||
params,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
AppSign.appSign(params);
|
||||
var res = await Request().get(
|
||||
Api.liveFeedIndex,
|
||||
queryParameters: params,
|
||||
@@ -290,11 +286,7 @@ abstract final class LiveHttp {
|
||||
'statistics': Constants.statisticsApp,
|
||||
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
};
|
||||
AppSign.appSign(
|
||||
params,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
AppSign.appSign(params);
|
||||
var res = await Request().get(
|
||||
Api.liveSecondList,
|
||||
queryParameters: params,
|
||||
@@ -340,11 +332,7 @@ abstract final class LiveHttp {
|
||||
'statistics': Constants.statisticsApp,
|
||||
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
};
|
||||
AppSign.appSign(
|
||||
params,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
AppSign.appSign(params);
|
||||
var res = await Request().get(
|
||||
Api.liveAreaList,
|
||||
queryParameters: params,
|
||||
@@ -377,11 +365,7 @@ abstract final class LiveHttp {
|
||||
'statistics': Constants.statisticsApp,
|
||||
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
};
|
||||
AppSign.appSign(
|
||||
params,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
AppSign.appSign(params);
|
||||
var res = await Request().get(
|
||||
Api.getLiveFavTag,
|
||||
queryParameters: params,
|
||||
@@ -419,11 +403,7 @@ abstract final class LiveHttp {
|
||||
'statistics': Constants.statisticsApp,
|
||||
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
};
|
||||
AppSign.appSign(
|
||||
data,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
AppSign.appSign(data);
|
||||
var res = await Request().post(
|
||||
Api.setLiveFavTag,
|
||||
data: data,
|
||||
@@ -459,11 +439,7 @@ abstract final class LiveHttp {
|
||||
'statistics': Constants.statisticsApp,
|
||||
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
};
|
||||
AppSign.appSign(
|
||||
params,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
AppSign.appSign(params);
|
||||
var res = await Request().get(
|
||||
Api.liveRoomAreaList,
|
||||
queryParameters: params,
|
||||
@@ -502,11 +478,7 @@ abstract final class LiveHttp {
|
||||
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
'type': type.name,
|
||||
};
|
||||
AppSign.appSign(
|
||||
params,
|
||||
Constants.appKey,
|
||||
Constants.appSec,
|
||||
);
|
||||
AppSign.appSign(params);
|
||||
var res = await Request().get(
|
||||
Api.liveSearch,
|
||||
queryParameters: params,
|
||||
|
||||
@@ -26,6 +26,7 @@ import 'package:PiliPlus/models_new/video/video_note_list/data.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_play_info/data.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_relation/data.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
import 'package:PiliPlus/utils/app_sign.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/global_data.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
@@ -1060,4 +1061,39 @@ class VideoHttp {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<LoadingState<PlayUrlModel>> tvPlayUrl({
|
||||
required int cid,
|
||||
required int objectId, // aid, epid
|
||||
required int playurlType, // ugc 1, pgc 2
|
||||
int? qn,
|
||||
}) async {
|
||||
final accessKey = Accounts.accountMode[AccountType.video.index].accessKey;
|
||||
final params = {
|
||||
'access_key': ?accessKey,
|
||||
'actionKey': 'appkey',
|
||||
'appkey': Constants.appKey,
|
||||
'cid': cid,
|
||||
'fourk': 1,
|
||||
'is_proj': 1,
|
||||
'mobile_access_key': ?accessKey,
|
||||
'object_id': objectId,
|
||||
'mobi_app': 'android',
|
||||
'platform': 'android',
|
||||
'playurl_type': playurlType,
|
||||
'protocol': 0,
|
||||
'qn': qn ?? 80,
|
||||
'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
};
|
||||
AppSign.appSign(params);
|
||||
final res = await Request().get(
|
||||
Api.tvPlayUrl,
|
||||
queryParameters: params,
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return Success(PlayUrlModel.fromJson(res.data['data']));
|
||||
} else {
|
||||
return Error(res.data['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
130
lib/pages/dlna/view.dart
Normal file
130
lib/pages/dlna/view.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
|
||||
import 'package:dlna_dart/dlna.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class DLNAPage extends StatefulWidget {
|
||||
const DLNAPage({super.key});
|
||||
|
||||
@override
|
||||
State<DLNAPage> createState() => _DLNAPageState();
|
||||
}
|
||||
|
||||
class _DLNAPageState extends State<DLNAPage> {
|
||||
final _searcher = DLNAManager();
|
||||
final Map<String, DLNADevice> _deviceList = {};
|
||||
late final _url = Get.parameters['url']!;
|
||||
late final _title = Get.parameters['title'];
|
||||
|
||||
Timer? _timer;
|
||||
bool _isSearching = false;
|
||||
DLNADevice? _lastDevice;
|
||||
String? _lastDeviceKey;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_onSearch(isInit: true);
|
||||
}
|
||||
|
||||
Future<void> _onSearch({bool isInit = false}) async {
|
||||
if (_isSearching) return;
|
||||
_isSearching = true;
|
||||
if (!isInit && mounted) {
|
||||
_lastDevice = null;
|
||||
_deviceList.clear();
|
||||
setState(() {});
|
||||
}
|
||||
final deviceManager = await _searcher.start();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_timer = Timer(const Duration(seconds: 20), _searcher.stop);
|
||||
await for (final deviceList in deviceManager.devices.stream) {
|
||||
if (mounted) {
|
||||
_deviceList.addAll(deviceList);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_searcher.stop();
|
||||
_lastDevice = null;
|
||||
_lastDeviceKey = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = ColorScheme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('投屏'),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '搜索',
|
||||
onPressed: _onSearch,
|
||||
icon: const Icon(Icons.refresh),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
if (_isSearching) linearLoading,
|
||||
ViewSliverSafeArea(sliver: _buildBody(colorScheme)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(ColorScheme colorScheme) {
|
||||
if (!_isSearching && _deviceList.isEmpty) {
|
||||
return HttpError(
|
||||
errMsg: '没有设备',
|
||||
onReload: _onSearch,
|
||||
);
|
||||
}
|
||||
if (_deviceList.isNotEmpty) {
|
||||
final keys = _deviceList.keys.toList();
|
||||
return SliverList.builder(
|
||||
itemCount: keys.length,
|
||||
itemBuilder: (context, index) {
|
||||
final key = keys[index];
|
||||
final device = _deviceList[key]!;
|
||||
final isCurr = key == _lastDeviceKey;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
device.info.friendlyName,
|
||||
style: isCurr ? TextStyle(color: colorScheme.primary) : null,
|
||||
),
|
||||
subtitle: Text(key),
|
||||
onTap: () async {
|
||||
if (isCurr) return;
|
||||
_lastDevice?.pause();
|
||||
_lastDevice = device;
|
||||
_lastDeviceKey = key;
|
||||
setState(() {});
|
||||
await device.setUrl(_url, title: _title ?? '');
|
||||
await device.play();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SliverToBoxAdapter();
|
||||
}
|
||||
}
|
||||
@@ -1991,4 +1991,50 @@ class VideoDetailController extends GetxController
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onCast() async {
|
||||
SmartDialog.showLoading();
|
||||
final res = await VideoHttp.tvPlayUrl(
|
||||
cid: cid.value,
|
||||
objectId: epId ?? aid,
|
||||
playurlType: epId != null ? 2 : 1,
|
||||
qn: currentVideoQa.value?.code,
|
||||
);
|
||||
SmartDialog.dismiss();
|
||||
if (res.isSuccess) {
|
||||
final PlayUrlModel data = res.data;
|
||||
final first = data.durl?.firstOrNull;
|
||||
final url = first?.backupUrl?.lastOrNull ?? first?.url;
|
||||
if (url == null || url.isEmpty) {
|
||||
SmartDialog.showToast('不支持投屏');
|
||||
return;
|
||||
}
|
||||
String? title;
|
||||
try {
|
||||
if (isUgc) {
|
||||
title = Get.find<UgcIntroController>(
|
||||
tag: heroTag,
|
||||
).videoDetail.value.title;
|
||||
} else {
|
||||
title = Get.find<PgcIntroController>(
|
||||
tag: heroTag,
|
||||
).videoDetail.value.title;
|
||||
}
|
||||
} catch (_) {
|
||||
if (kDebugMode) rethrow;
|
||||
}
|
||||
if (kDebugMode) {
|
||||
debugPrint(title);
|
||||
}
|
||||
Get.toNamed(
|
||||
'/dlna',
|
||||
parameters: {
|
||||
'url': url,
|
||||
'title': ?title,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2413,23 +2413,41 @@ class HeaderControlState extends State<HeaderControl> {
|
||||
},
|
||||
),
|
||||
if (!isFileSource) ...[
|
||||
if ((!isFSOrPip && videoDetailCtr.isUgc))
|
||||
if (!isFSOrPip) ...[
|
||||
if (videoDetailCtr.isUgc)
|
||||
SizedBox(
|
||||
width: 42,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: '听音频',
|
||||
style: const ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: videoDetailCtr.toAudioPage,
|
||||
icon: const Icon(
|
||||
Icons.headphones_outlined,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 42,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
tooltip: '听音频',
|
||||
tooltip: '投屏',
|
||||
style: const ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: videoDetailCtr.toAudioPage,
|
||||
onPressed: videoDetailCtr.onCast,
|
||||
icon: const Icon(
|
||||
Icons.headphones_outlined,
|
||||
Icons.cast,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (plPlayerController.enableSponsorBlock == true)
|
||||
SizedBox(
|
||||
width: 42,
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:PiliPlus/pages/article_list/view.dart';
|
||||
import 'package:PiliPlus/pages/audio/view.dart';
|
||||
import 'package:PiliPlus/pages/blacklist/view.dart';
|
||||
import 'package:PiliPlus/pages/danmaku_block/view.dart';
|
||||
import 'package:PiliPlus/pages/dlna/view.dart';
|
||||
import 'package:PiliPlus/pages/download/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics/view.dart';
|
||||
import 'package:PiliPlus/pages/dynamics_create_vote/view.dart';
|
||||
@@ -229,6 +230,7 @@ class Routes {
|
||||
CustomGetPage(name: '/followed', page: () => const FollowedPage()),
|
||||
CustomGetPage(name: '/sameFollowing', page: () => const FollowSamePage()),
|
||||
CustomGetPage(name: '/download', page: () => const DownloadPage()),
|
||||
CustomGetPage(name: '/dlna', page: () => const DLNAPage()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import 'package:crypto/crypto.dart';
|
||||
|
||||
abstract class AppSign {
|
||||
static void appSign(
|
||||
Map<String, dynamic> params, [
|
||||
Map<String, dynamic> params, {
|
||||
String appkey = Constants.appKey,
|
||||
String appsec = Constants.appSec,
|
||||
]) {
|
||||
}) {
|
||||
params['appkey'] = appkey;
|
||||
var searchParams = Uri(
|
||||
queryParameters: params.map(
|
||||
|
||||
Reference in New Issue
Block a user