ScreenPlayer.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
  2. import classNames from 'classnames';
  3. import LivePlayer from './index';
  4. import { Button, Dropdown, Menu, Popconfirm, Popover, Radio, Tooltip } from 'antd';
  5. import { createSchemaField } from '@formily/react';
  6. import { Form, FormItem, Input } from '@formily/antd';
  7. import { useFullscreen } from 'ahooks';
  8. import './index.less';
  9. import { DeleteOutlined, QuestionCircleOutlined } from '@ant-design/icons';
  10. import Service from './service';
  11. import MediaTool from '@/components/Player/mediaTool';
  12. import { createForm } from '@formily/core';
  13. import { onlyMessage } from '@/utils/util';
  14. import { Empty } from '@/components';
  15. type Player = {
  16. id?: string;
  17. url?: string;
  18. channelId?: string;
  19. key: string;
  20. show: boolean;
  21. };
  22. interface ScreenProps {
  23. url?: string;
  24. id?: string;
  25. channelId: string;
  26. className?: string;
  27. historyHandle?: (deviceId: string, channelId: string) => string;
  28. /**
  29. *
  30. * @param id 当前选中播发视频ID
  31. * @param type 当前操作动作
  32. */
  33. onMouseDown?: (deviceId: string, channelId: string, type: string) => void;
  34. /**
  35. *
  36. * @param id 当前选中播发视频ID
  37. * @param type 当前操作动作
  38. */
  39. onMouseUp?: (deviceId: string, channelId: string, type: string) => void;
  40. showScreen?: boolean;
  41. }
  42. const service = new Service();
  43. const DEFAULT_SAVE_CODE = 'screen-save';
  44. const useCallbackState = <T extends object>(olValue: T): [T, Function] => {
  45. const cbRef = useRef<Function>();
  46. const [players, setData] = useState<T>(olValue);
  47. useEffect(() => {
  48. if (cbRef.current) {
  49. cbRef.current(players);
  50. }
  51. }, [players]);
  52. return [
  53. players,
  54. function (value: T, callback?: Function) {
  55. cbRef.current = callback;
  56. setData(value);
  57. },
  58. ];
  59. };
  60. export default forwardRef((props: ScreenProps, ref) => {
  61. const [screen, setScreen] = useState(1);
  62. const [players, setPlayers] = useCallbackState<Player[]>([]);
  63. const [playerActive, setPlayerActive] = useState(0);
  64. const [historyList, setHistoryList] = useState<any>([]);
  65. const [visible, setVisible] = useState(false);
  66. const [loading, setLoading] = useState(false);
  67. const fullscreenRef = useRef(null);
  68. const [isFullscreen, { setFull }] = useFullscreen(fullscreenRef);
  69. const SchemaField = createSchemaField({
  70. components: {
  71. FormItem,
  72. Input,
  73. },
  74. });
  75. const historyForm = createForm();
  76. const reloadPlayer = useCallback(
  77. (id: string, channelId: string, url: string, index: number) => {
  78. const olPlayers = [...players];
  79. olPlayers[index] = {
  80. id: '',
  81. channelId: '',
  82. url: '',
  83. key: olPlayers[index].key,
  84. show: true,
  85. };
  86. const newPlayer = {
  87. id,
  88. url,
  89. channelId,
  90. key: olPlayers[index].key,
  91. show: true,
  92. };
  93. setPlayers([...olPlayers]);
  94. setTimeout(() => {
  95. olPlayers[index] = newPlayer;
  96. setPlayers(olPlayers);
  97. }, 1000);
  98. },
  99. [players],
  100. );
  101. const replaceVideo = useCallback(
  102. (id: string, channelId: string, url: string) => {
  103. const olPlayers = [...players];
  104. const newPlayer = {
  105. id,
  106. url,
  107. channelId,
  108. key: olPlayers[playerActive].key,
  109. show: true,
  110. };
  111. if (olPlayers[playerActive].url === url) {
  112. // 刷新视频
  113. reloadPlayer(id, channelId, url, playerActive);
  114. } else {
  115. olPlayers[playerActive] = newPlayer;
  116. setPlayers(olPlayers);
  117. }
  118. if (playerActive === screen - 1) {
  119. // 当前位置为分屏最后一位
  120. setPlayerActive(0);
  121. } else {
  122. setPlayerActive(playerActive + 1);
  123. }
  124. },
  125. [players, playerActive, screen, props.showScreen],
  126. );
  127. const handleHistory = useCallback(
  128. (item: any) => {
  129. if (props.historyHandle) {
  130. const log = JSON.parse(item.content || '{}');
  131. setScreen(log.screen);
  132. const oldPlayers = [...players];
  133. setPlayers(
  134. oldPlayers.map((oldPlayer, index) => {
  135. oldPlayer.show = false;
  136. if (index < log.screen) {
  137. const { deviceId, channelId } = log.players[index];
  138. return {
  139. ...oldPlayer,
  140. id: deviceId,
  141. channelId: deviceId,
  142. url: deviceId ? props.historyHandle!(deviceId, channelId) : '',
  143. show: true,
  144. };
  145. }
  146. return oldPlayer;
  147. }),
  148. );
  149. }
  150. },
  151. [players, props.historyHandle],
  152. );
  153. const getHistory = async () => {
  154. const resp = await service.history.query(DEFAULT_SAVE_CODE);
  155. if (resp.status === 200) {
  156. setHistoryList(resp.result);
  157. }
  158. };
  159. const deleteHistory = async (id: string) => {
  160. const resp = await service.history.remove(DEFAULT_SAVE_CODE, id);
  161. if (resp.status === 200) {
  162. getHistory();
  163. setVisible(false);
  164. }
  165. };
  166. const saveHistory = useCallback(async () => {
  167. const historyValue = await historyForm.submit<{ alias: string }>();
  168. const param = {
  169. name: historyValue.alias,
  170. content: JSON.stringify({
  171. screen: screen,
  172. players: players.map((item) => ({ deviceId: item.id, channelId: item.channelId })),
  173. }),
  174. };
  175. setLoading(true);
  176. const resp = await service.history.save(DEFAULT_SAVE_CODE, param);
  177. setLoading(false);
  178. if (resp.status === 200) {
  179. setVisible(false);
  180. getHistory();
  181. onlyMessage('保存成功!');
  182. } else {
  183. onlyMessage('保存失败', 'error');
  184. // message.error('保存失败');
  185. }
  186. }, [players, screen, historyForm]);
  187. const mediaInit = () => {
  188. const newArr = [];
  189. for (let i = 0; i < 9; i++) {
  190. newArr.push({
  191. id: '',
  192. channelId: '',
  193. url: '',
  194. key: 'time_' + new Date().getTime() + i,
  195. show: i === 0,
  196. });
  197. }
  198. setPlayers(newArr);
  199. };
  200. const screenChange = (index: number) => {
  201. setPlayers(
  202. players.map((item, i) => {
  203. return {
  204. id: '',
  205. channelId: '',
  206. url: '',
  207. updateTime: 0,
  208. key: item.key,
  209. show: i < index,
  210. };
  211. }),
  212. );
  213. setPlayerActive(0);
  214. setScreen(index);
  215. };
  216. useEffect(() => {
  217. // 查看当前 播放视频位置,如果当前视频位置有视频在播放,则替换
  218. if (props.url && props.id) {
  219. replaceVideo(props.id, props.channelId, props.url);
  220. }
  221. }, [props.url]);
  222. useEffect(() => {
  223. if (props.showScreen !== false) {
  224. getHistory();
  225. }
  226. mediaInit();
  227. }, []);
  228. useImperativeHandle(ref, () => ({
  229. replaceVideo: replaceVideo,
  230. }));
  231. const screenClass = `screen-${screen}`;
  232. const DropdownMenu = (
  233. <Menu>
  234. {historyList.length ? (
  235. historyList.map((item: any) => {
  236. return (
  237. <Menu.Item key={item.id}>
  238. <span
  239. onClick={() => {
  240. handleHistory(item);
  241. }}
  242. style={{ padding: '0 4px' }}
  243. >
  244. {item.name}
  245. </span>
  246. <Popconfirm
  247. title={'确认删除'}
  248. onConfirm={(e) => {
  249. e?.stopPropagation();
  250. deleteHistory(item.key);
  251. }}
  252. >
  253. <DeleteOutlined
  254. onClick={(e) => {
  255. e.stopPropagation();
  256. }}
  257. />
  258. </Popconfirm>
  259. </Menu.Item>
  260. );
  261. })
  262. ) : (
  263. <Empty />
  264. )}
  265. </Menu>
  266. );
  267. return (
  268. <div className={classNames('live-player-warp', props.className)}>
  269. <div className={'live-player-content'}>
  270. {props.showScreen !== false && (
  271. <div className={'player-screen-tool'}>
  272. <>
  273. <div>
  274. <Radio.Group
  275. options={[
  276. { label: '单屏', value: 1 },
  277. { label: '四分屏', value: 4 },
  278. { label: '九分屏', value: 9 },
  279. { label: '全屏', value: 0 },
  280. ]}
  281. value={screen}
  282. onChange={(e) => {
  283. if (e.target.value) {
  284. screenChange(e.target.value);
  285. } else {
  286. // 全屏操作
  287. setFull();
  288. }
  289. }}
  290. optionType={'button'}
  291. buttonStyle={'solid'}
  292. />
  293. {/*<Tooltip*/}
  294. {/* title={''}*/}
  295. {/*>*/}
  296. {/* <QuestionCircleOutlined />*/}
  297. {/*</Tooltip>*/}
  298. </div>
  299. <div
  300. className={'screen-tool-save'}
  301. style={{ display: 'flex', flexDirection: 'row-reverse', alignItems: 'center' }}
  302. >
  303. <Popover
  304. content={
  305. <Form style={{ width: '217px' }} form={historyForm}>
  306. <SchemaField
  307. schema={{
  308. type: 'object',
  309. properties: {
  310. alias: {
  311. 'x-decorator': 'FormItem',
  312. 'x-component': 'Input.TextArea',
  313. 'x-validator': [
  314. {
  315. max: 64,
  316. message: '最多可输入64个字符',
  317. },
  318. {
  319. required: true,
  320. message: '请输入名称',
  321. },
  322. ],
  323. },
  324. },
  325. }}
  326. />
  327. <Button
  328. type={'primary'}
  329. onClick={saveHistory}
  330. loading={loading}
  331. style={{ width: '100%', marginRight: 16 }}
  332. >
  333. 保存
  334. </Button>
  335. </Form>
  336. }
  337. title="分屏名称"
  338. trigger="click"
  339. visible={visible}
  340. onVisibleChange={(v) => {
  341. setVisible(v);
  342. }}
  343. >
  344. <Dropdown.Button
  345. type={'primary'}
  346. overlay={DropdownMenu}
  347. onClick={() => {
  348. setVisible(true);
  349. }}
  350. >
  351. 保存
  352. </Dropdown.Button>
  353. </Popover>
  354. <Tooltip title={'可保存分屏配置记录'}>
  355. <QuestionCircleOutlined style={{ marginLeft: 8 }} />
  356. </Tooltip>
  357. </div>
  358. </>
  359. </div>
  360. )}
  361. <div className={'player-body'}>
  362. <div className={classNames('player-screen', screenClass)} ref={fullscreenRef}>
  363. {players.map((item, index) => {
  364. return (
  365. <div
  366. key={item.key}
  367. className={classNames('player-screen-item', {
  368. active: props.showScreen !== false && playerActive === index && !isFullscreen,
  369. 'full-screen': isFullscreen,
  370. })}
  371. style={{ display: item.show ? 'block' : 'none' }}
  372. onClick={() => {
  373. setPlayerActive(index);
  374. }}
  375. >
  376. <div
  377. className={'media-btn-refresh'}
  378. style={{ display: item.url ? 'block' : 'none' }}
  379. onClick={(e) => {
  380. e.stopPropagation();
  381. if (item.url) {
  382. reloadPlayer(item.id!, item.channelId!, item.url!, index);
  383. }
  384. }}
  385. >
  386. 刷新
  387. </div>
  388. <LivePlayer url={item.url} />
  389. </div>
  390. );
  391. })}
  392. </div>
  393. </div>
  394. </div>
  395. <MediaTool
  396. onMouseDown={(type) => {
  397. const { id, channelId } = players[playerActive];
  398. if (id && channelId && props.onMouseDown) {
  399. props.onMouseDown(id, channelId, type);
  400. }
  401. }}
  402. onMouseUp={(type) => {
  403. const { id, channelId } = players[playerActive];
  404. if (props.onMouseUp && id && channelId) {
  405. props.onMouseUp(id, channelId, type);
  406. }
  407. }}
  408. />
  409. </div>
  410. );
  411. });