YSLiveAnchor.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'package:flutter/cupertino.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutterappfuyou/code/base/YSNetWorking.dart';
  7. import 'package:flutterappfuyou/code/base/YSTools.dart';
  8. import 'package:flutterappfuyou/code/live/view/YSLiveUserView.dart';
  9. import 'package:live_flutter_plugin/v2_tx_live_def.dart';
  10. import 'package:live_flutter_plugin/v2_tx_live_pusher.dart';
  11. import 'package:live_flutter_plugin/v2_tx_live_pusher_observer.dart';
  12. import 'package:live_flutter_plugin/widget/v2_tx_live_video_widget.dart';
  13. import 'package:wakelock/wakelock.dart';
  14. class YSLiveAnchor extends StatefulWidget {
  15. final int liveId;
  16. final isSelect;
  17. const YSLiveAnchor({Key key, this.liveId, this.isSelect = false}) : super(key: key);
  18. @override
  19. _YSLiveAnchorState createState() => _YSLiveAnchorState();
  20. }
  21. class _YSLiveAnchorState extends State<YSLiveAnchor> {
  22. String _pushUrl = '';
  23. WebSocket _socket;
  24. List _userArray = [];
  25. int _userCount = 0;
  26. bool _isMute = false;
  27. bool _isPause = false;
  28. bool _isFont = true;
  29. @override
  30. void initState() {
  31. if(widget.isSelect){
  32. Future.delayed(Duration(seconds: 0)).then((value) {
  33. _getStartData();
  34. });
  35. }
  36. super.initState();
  37. }
  38. _getStartData() async{
  39. Map dict = await ysRequestHttp(context, requestType.get, 'train/live2/start', {'live_id':widget.liveId});
  40. if(dict!=null){
  41. Map data = dict['data'];
  42. _pushUrl = data['push_url'];
  43. User().castAvatar = data['cast_avatar'];
  44. User().castName = data['cast_name'];
  45. User().stream = data['live_stream']??'';
  46. User.instance.isAnchor = true;
  47. setState(() {});
  48. }
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. return NotificationListener<CustomerValueNotification>(
  53. onNotification: (notification){
  54. Map data = notification.value;
  55. String type = data['type'];
  56. if(type=='headcount'){
  57. _userArray = data['value']['users']??[];
  58. _userCount = data['value']['total']??0;
  59. setState(() {});
  60. }else if(type=='mute'){
  61. _isMute = !_isMute;
  62. setState(() {});
  63. }else if(type=='pause'||type=='resume'){
  64. if(type=='pause'){
  65. liveKey.currentState._pauseLive();
  66. }else{
  67. liveKey.currentState._resumeLive();
  68. }
  69. _isPause = type=='pause';
  70. setState(() {});
  71. }else if(type=='staff'){
  72. List users = data['value']['users']??[];
  73. ysShowBottomAlertView(context, YSUsersAlertView(users: users,),isBarr: true);
  74. }
  75. return true;
  76. },
  77. child: WillPopScope(
  78. onWillPop: () async{
  79. return false;
  80. },
  81. child: Scaffold(
  82. backgroundColor: Colors.black,
  83. body: Container(
  84. width: ysWidth(context),
  85. height: ysHeight(context),
  86. child: Stack(
  87. children: [
  88. _pushUrl.isNotEmpty?YSAnchorLiveView(
  89. key: liveKey,pushStr: _pushUrl,
  90. ):Image.asset('lib/images/图.png',fit: BoxFit.fill,width: ysWidth(context),height: ysHeight(context),),
  91. Container(
  92. width: ysWidth(context),
  93. height: ysHeight(context),
  94. padding: EdgeInsets.only(top: ysTOP(context)+15,left: 10,right: 10,bottom: 10),
  95. child: Column(
  96. children: [
  97. Container(
  98. height: 40,
  99. child: Row(
  100. children: [
  101. Container(
  102. width: (ysWidth(context)-20)*0.3,
  103. decoration: BoxDecoration(
  104. color: Colors.black,
  105. borderRadius: BorderRadius.all(Radius.circular(50))
  106. ),
  107. child: User().castAvatar!=null?Row(
  108. children: [
  109. Container(
  110. height: 40,
  111. width: 40,
  112. decoration: BoxDecoration(
  113. borderRadius: BorderRadius.all(Radius.circular(50)),
  114. image: DecorationImage(image: NetworkImage(User().castAvatar))
  115. ),
  116. ),
  117. Container(
  118. width: (ysWidth(context)-20)*0.3-40,
  119. padding: EdgeInsets.only(left: 5,right: 5),
  120. child: Text(User().castName,style: TextStyle(fontSize: 10,color: Colors.white,fontWeight: FontWeight.bold,),
  121. maxLines: 1,overflow: TextOverflow.ellipsis,),
  122. )
  123. ],
  124. ):Container(),
  125. ),
  126. GestureDetector(
  127. onTap: (){
  128. if(_socket==null)return;
  129. _socket.add(jsonEncode({'type':'staff'}));
  130. // Navigator.of(context).push(
  131. // CupertinoPageRoute(builder: (context){
  132. // return YSLiveUser();
  133. // })
  134. // );
  135. },
  136. behavior: HitTestBehavior.opaque,
  137. child: Row(
  138. children: [
  139. Container(
  140. width: (ysWidth(context)-20)*0.55,
  141. padding: EdgeInsets.only(top: 5,bottom: 5,left: 10),
  142. child: ListView.builder(
  143. itemBuilder: (context,index){
  144. Map item = _userArray[index];
  145. return Container(
  146. height: 30,
  147. width: 30,
  148. decoration: BoxDecoration(
  149. borderRadius: BorderRadius.all(Radius.circular(50)),
  150. color: Colors.white,
  151. image: DecorationImage(image: NetworkImage(item['avatar']),fit: BoxFit.cover)
  152. ),
  153. );
  154. },
  155. itemCount: _userArray.length,
  156. scrollDirection: Axis.horizontal,
  157. ),
  158. ),
  159. Container(
  160. width: (ysWidth(context)-20)*0.15,
  161. decoration: BoxDecoration(
  162. color: Colors.black,
  163. borderRadius: BorderRadius.all(Radius.circular(50))
  164. ),
  165. alignment: Alignment.center,
  166. child: Text('$_userCount',style: TextStyle(fontSize: 12,color: Colors.white),),
  167. )
  168. ],
  169. ),
  170. )
  171. ],
  172. ),
  173. ),
  174. Container(
  175. height: ysHeight(context)-ysTOP(context)-125,
  176. alignment: Alignment.bottomRight,
  177. child: _pushUrl.isEmpty&&widget.isSelect==false?Column(
  178. mainAxisSize: MainAxisSize.min,
  179. crossAxisAlignment: CrossAxisAlignment.end,
  180. children: [
  181. Text('直播尚未开始,您是否需要开始直播?',style: TextStyle(fontSize: 18,color: Colors.white,fontWeight: FontWeight.bold),),
  182. GestureDetector(
  183. onTap: () {
  184. _getStartData();
  185. },
  186. child: Container(
  187. height: 44,
  188. width: 150,
  189. decoration: BoxDecoration(
  190. color: Color(0xFFE36085),
  191. borderRadius: BorderRadius.all(Radius.circular(5))
  192. ),
  193. margin: EdgeInsets.only(top: 30,bottom: 50),
  194. alignment: Alignment.center,
  195. child: Text('立即直播',style: TextStyle(fontSize: 18,color: Colors.white),),
  196. ),
  197. )
  198. ],
  199. ):Container(),
  200. ),
  201. _pushUrl.isEmpty?Container(
  202. height: 60,
  203. alignment: Alignment.centerRight,
  204. child: GestureDetector(
  205. onTap: (){
  206. Navigator.of(context).pop('');
  207. Navigator.of(context).pop('');
  208. Navigator.of(context).pop('');
  209. },
  210. child: Image.asset('lib/images/退出.png',height: 20,width: 20,),
  211. )
  212. ):Container(
  213. alignment: Alignment.centerRight,
  214. height: 60,
  215. child: Row(
  216. mainAxisSize: MainAxisSize.min,
  217. children: [
  218. GestureDetector(
  219. child: Image.asset('lib/images/${_isMute?'取消禁言':'禁言'}.png',height: 20,width: 20,color: Colors.white,),
  220. onTap: (){
  221. if(_isMute){
  222. _socket.add(jsonEncode({'type':'mute','mute_time':1}));
  223. }else{
  224. _socket.add(jsonEncode({'type':'mute','mute_time':0}));
  225. }
  226. },
  227. ),
  228. Container(
  229. margin: EdgeInsets.only(left: 20,right: 20),
  230. child: GestureDetector(
  231. onTap: (){
  232. if(_isPause){
  233. _socket.add(jsonEncode({'type':'resume'}));
  234. }else{
  235. _socket.add(jsonEncode({'type':'pause'}));
  236. }
  237. },
  238. child: Image.asset('lib/images/${_isPause?'取消暂停':'暂停'}.png',height: 20,width: 20,color: Colors.white,),
  239. )
  240. ),
  241. Container(
  242. margin: EdgeInsets.only(right: 20),
  243. child: GestureDetector(
  244. onTap: () async{
  245. _isFont = !_isFont;
  246. liveKey.currentState._changeCameraLive(_isFont);
  247. setState(() {});
  248. },
  249. child: Icon(_isFont?Icons.camera_front:Icons.video_camera_back,color: Colors.white,)
  250. ),
  251. ),
  252. GestureDetector(
  253. onTap: () {
  254. ysShowCenterAlertView(context, YSTipsAlertView(
  255. tipsStr: '是否退出直播?',
  256. valueSetter: (value) async{
  257. if(value){
  258. Map dict = await ysRequestHttp(context, requestType.get, 'train/live2/close', {'live_id':widget.liveId});
  259. if(dict!=null){
  260. if(_socket!=null){
  261. _socket.add(jsonEncode({'type':'leave'}));
  262. }
  263. liveKey.currentState._stopPush();
  264. Navigator.of(context).pop('');
  265. Navigator.of(context).pop('');
  266. Navigator.of(context).pop('');
  267. }
  268. }
  269. },
  270. ));
  271. },
  272. child: Image.asset('lib/images/退出.png',height: 20,width: 20,color: Colors.white,),
  273. )
  274. ],
  275. ),
  276. ),
  277. ],
  278. ),
  279. ),
  280. if(_pushUrl.isNotEmpty)Positioned(
  281. bottom: 70,
  282. left: 10,
  283. child:YSTalkView(
  284. postSocket: (socket){
  285. _socket = socket;
  286. if(_pushUrl.isNotEmpty){
  287. Future.delayed(Duration(seconds: 2)).then((value) {
  288. _socket.add(jsonEncode({'type':'enter'}));
  289. });
  290. }
  291. },
  292. )
  293. )
  294. ],
  295. ),
  296. ),
  297. ),
  298. ),
  299. );
  300. }
  301. }
  302. class YSTalkView extends StatefulWidget {
  303. final ValueSetter<WebSocket> postSocket;
  304. const YSTalkView({Key key, this.postSocket}) : super(key: key);
  305. @override
  306. _YSTalkViewState createState() => _YSTalkViewState();
  307. }
  308. class _YSTalkViewState extends State<YSTalkView> {
  309. List _dataArray = [];
  310. static WebSocket _socket;
  311. Timer _timer;
  312. ScrollController _scrollController = ScrollController();
  313. @override
  314. void initState() {
  315. _getSocket();
  316. super.initState();
  317. }
  318. //wss://v2fy.niwoshenghuo.com/websocket
  319. //ws://101.43.97.222:868
  320. //ws://114.115.176.164:8686
  321. _getSocket() async{
  322. WebSocket.connect('wss://v2fy.niwoshenghuo.com/websocket').then((socket) {
  323. _socket = socket;
  324. widget.postSocket(_socket);
  325. socket.listen(_onData, cancelOnError: false);
  326. _timer =Timer.periodic(Duration(seconds: 10), (timer) {
  327. _socket.add('heartbeat');
  328. });
  329. }).catchError((e){
  330. LogUtil.d("Unable to connect: $e");
  331. _getSocket(); // 连接超时,重新建立连接
  332. });
  333. }
  334. _onData(event) async{
  335. Map dict = jsonDecode(event);
  336. LogUtil.d("---onData---$dict");
  337. if(dict['type']=='connection'){
  338. Map data = dict['data'];
  339. _dataArray.add({'type':0,'content':data['content']});
  340. setState(() {});
  341. Map message = {};
  342. message['type'] = 'bind';
  343. message['live_stream'] = User().stream;
  344. message['uid'] = data['uid'];
  345. message['user'] = {'username':User().castName??User().name,'avatar':User().castAvatar??User().avatar,'is_owner':User().isAnchor};
  346. _socket.add(jsonEncode(message));
  347. }else if(dict['type']=='message'){
  348. var data = dict['data'];
  349. if(data is List){
  350. _dataArray.add({'type':1,'content':User().content,'name':User().castName??User().name,'avatar':User().castAvatar??User().name});
  351. }else{
  352. Map user = data['user'];
  353. _dataArray.add({'type':1,'content':data['content'],'name':user['username'],'avatar':user['avatar']});
  354. }
  355. setState(() {});
  356. _scrollController.jumpTo(_scrollController.position.maxScrollExtent+50);
  357. }else if(dict['type']=='headcount'||dict['type']=='staff'||dict['type']=='mute'||dict['type']=='pause'||dict['type']=='resume'||dict['type']=='enter'||dict['type']=='staff'){
  358. var data = dict['data'];
  359. CustomerValueNotification({'type':dict['type'],'value':data}).dispatch(context);
  360. }else if(dict['type']=='leave'){
  361. CustomerValueNotification({'type':dict['type'],'value':{}}).dispatch(context);
  362. }else if(dict['type']=='bind'){
  363. _socket.add(jsonEncode({'type':'headcount'}));
  364. }else if(dict['type']=='heartbeat'){
  365. var data = dict['data'];
  366. CustomerValueNotification({'type':'headcount','value':data}).dispatch(context);
  367. }
  368. }
  369. @override
  370. void dispose() {
  371. _socket.close();
  372. if(_timer!=null){
  373. if(_timer.isActive)_timer.cancel();
  374. }
  375. super.dispose();
  376. }
  377. @override
  378. Widget build(BuildContext context) {
  379. return Container(
  380. height: ysHeight(context)*0.4,
  381. width: ysWidth(context)*0.75,
  382. child: ListView.separated(
  383. controller: _scrollController,
  384. padding: EdgeInsets.only(top: 10,bottom: 10),
  385. itemBuilder: (context,index){
  386. Map item = _dataArray[index];
  387. int type = item['type'];
  388. return Container(
  389. padding: EdgeInsets.only(left: 10,right: 10,top: 5,bottom: 5),
  390. decoration: BoxDecoration(
  391. color: Colors.black12,
  392. borderRadius: BorderRadius.all(Radius.circular(3))
  393. ),
  394. child: type==0?Text('${item['content']}',style: TextStyle(fontSize: 13,color: Colors.orange),):Row(
  395. mainAxisSize: MainAxisSize.min,
  396. crossAxisAlignment: CrossAxisAlignment.center,
  397. children: [
  398. Container(
  399. height: 20,
  400. width: 20,
  401. decoration: BoxDecoration(
  402. color: Colors.white,
  403. borderRadius: BorderRadius.all(Radius.circular(50)),
  404. image: DecorationImage(image: NetworkImage(item['avatar']),fit: BoxFit.cover)
  405. ),
  406. ),
  407. Container(
  408. padding: EdgeInsets.only(left: 5),
  409. constraints: BoxConstraints(maxWidth: ysWidth(context)*0.75-40),
  410. child: RichText(
  411. text: TextSpan(
  412. text: '${item['name']}:',
  413. style: TextStyle(fontSize: 13,color: Color(0xFF89DCFF)),
  414. children: [
  415. TextSpan(
  416. text: '${item['content']}',
  417. style: TextStyle(fontSize: 13,color: Colors.white)
  418. )
  419. ]
  420. ),
  421. ),
  422. )
  423. ],
  424. ),
  425. );
  426. },
  427. separatorBuilder: (context,index){
  428. return Container(height: 5,);
  429. },
  430. itemCount: _dataArray.length
  431. ),
  432. );
  433. }
  434. }
  435. GlobalKey<_YSAnchorLiveViewState> liveKey = GlobalKey();
  436. class YSAnchorLiveView extends StatefulWidget {
  437. final String pushStr;
  438. const YSAnchorLiveView({Key key, this.pushStr = ''}) : super(key: key);
  439. @override
  440. _YSAnchorLiveViewState createState() => _YSAnchorLiveViewState();
  441. }
  442. class _YSAnchorLiveViewState extends State<YSAnchorLiveView> {
  443. V2TXLivePusher _livePusher;
  444. int _localViewId;
  445. bool _isFont = true;
  446. _initPusher() async{
  447. _livePusher = V2TXLivePusher(V2TXLiveMode.v2TXLiveModeRTMP);
  448. _livePusher.addListener(_onPusherObserver);
  449. }
  450. _onPusherObserver(V2TXLivePusherListenerType type, param) {
  451. debugPrint("==pusher listener type= ${type.toString()}");
  452. debugPrint("==pusher listener param= $param");
  453. }
  454. @override
  455. void initState() {
  456. Wakelock.enable();
  457. _initPusher();
  458. super.initState();
  459. }
  460. @override
  461. void dispose() async{
  462. Wakelock.disable();
  463. _stopPush();
  464. super.dispose();
  465. }
  466. @override
  467. Widget build(BuildContext context) {
  468. return V2TXLiveVideoWidget(
  469. onViewCreated: (viewId) async{
  470. _localViewId = viewId;
  471. _livePusher.setRenderViewID(_localViewId);
  472. _startPush();
  473. }
  474. );
  475. }
  476. _stopPush() async {
  477. await _livePusher?.stopMicrophone();
  478. await _livePusher?.stopCamera();
  479. await _livePusher?.stopPush();
  480. _livePusher.destroy();
  481. }
  482. _pauseLive(){
  483. _livePusher.stopCamera();
  484. _livePusher.stopMicrophone();
  485. }
  486. _resumeLive(){
  487. _livePusher.startCamera(_isFont);
  488. _livePusher.startMicrophone();
  489. }
  490. _changeCameraLive(bool isFont){
  491. _isFont = isFont;
  492. _livePusher.getDeviceManager().switchCamera(isFont);
  493. }
  494. _startPush() async {
  495. bool bool1 = await permissionHandler('microphone');
  496. bool bool2 = await permissionHandler('camera');
  497. if(bool1&&bool2){
  498. _livePusher.getBeautyManager().setBeautyLevel(5);
  499. _livePusher.startCamera(_isFont);
  500. _livePusher.startMicrophone();
  501. int result = await _livePusher.startPush(widget.pushStr);
  502. LogUtil.d('result=========$result');
  503. }
  504. }
  505. }