Просмотр исходного кода

feat(视频通道): 对接视频播放

xieyonghong 3 лет назад
Родитель
Сommit
d75b35848f

+ 104 - 79
src/components/Player/ScreenPlayer.tsx

@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
 import classNames from 'classnames';
 import LivePlayer from './index';
 import { Radio, Dropdown, Menu, Popover, Input, Button, Empty, Tooltip } from 'antd';
@@ -19,24 +19,27 @@ import Service from './service';
 type Player = {
   id?: string;
   url?: string;
+  channelId?: string;
+  key: string;
 };
 
 interface ScreenProps {
   url?: string;
   id?: string;
-  channelId?: string;
+  channelId: string;
+  className?: string;
   /**
    *
    * @param id 当前选中播发视频ID
    * @param type 当前操作动作
    */
-  onMouseDown?: (id: string, channelId: string, type: string) => void;
+  onMouseDown?: (deviceId: string, channelId: string, type: string) => void;
   /**
    *
    * @param id 当前选中播发视频ID
    * @param type 当前操作动作
    */
-  onMouseUp?: (id: string, channelId: string, type: string) => void;
+  onMouseUp?: (deviceId: string, channelId: string, type: string) => void;
   showScreen?: boolean;
 }
 
@@ -44,7 +47,7 @@ const service = new Service();
 
 const DEFAULT_SAVE_CODE = 'screen_save';
 
-export default (props: ScreenProps) => {
+export default forwardRef((props: ScreenProps, ref) => {
   const [screen, setScreen] = useState(1);
   const [players, setPlayers] = useState<Player[]>([]);
   const [playerActive, setPlayerActive] = useState(0);
@@ -56,21 +59,17 @@ export default (props: ScreenProps) => {
   const [isFullscreen, { setFull }] = useFullscreen(fullscreenRef);
 
   const replaceVideo = useCallback(
-    (id: string, url: string) => {
-      const videoIndex = players.findIndex((video) => video.url === url);
-      if (videoIndex === -1) {
-        players[playerActive] = { id, url };
-        setPlayers(players);
-
-        if (playerActive === screen) {
-          // 当前位置为分屏最后一位
-          setPlayerActive(0);
-        } else {
-          setPlayerActive(playerActive + 1);
-        }
+    (id: string, channelId: string, url: string) => {
+      players[playerActive] = { id, url, channelId, key: 'time_' + new Date().getTime() };
+      setPlayers(players);
+      if (playerActive === screen - 1) {
+        // 当前位置为分屏最后一位
+        setPlayerActive(0);
+      } else {
+        setPlayerActive(playerActive + 1);
       }
     },
-    [players, playerActive, screen],
+    [players, playerActive, screen, props.showScreen],
   );
 
   const handleHistory = (item: any) => {
@@ -109,16 +108,19 @@ export default (props: ScreenProps) => {
     }
   }, [players, historyTitle, screen]);
 
-  useEffect(() => {
-    const arr = new Array(screen).fill(1).map(() => ({ id: '', url: '' }));
+  const screenChange = (index: number) => {
+    const arr = new Array(index)
+      .fill(1)
+      .map(() => ({ id: '', channelId: '', url: '', key: 'time_' + new Date().getTime() }));
     setPlayers(arr);
     setPlayerActive(0);
-  }, [screen]);
+    setScreen(index);
+  };
 
   useEffect(() => {
     // 查看当前 播放视频位置,如果当前视频位置有视频在播放,则替换
     if (props.url && props.id) {
-      replaceVideo(props.id, props.url);
+      replaceVideo(props.id, props.channelId, props.url);
     }
   }, [props.url]);
 
@@ -126,8 +128,13 @@ export default (props: ScreenProps) => {
     if (props.showScreen !== false) {
       getHistory();
     }
+    screenChange(1);
   }, []);
 
+  useImperativeHandle(ref, () => ({
+    replaceVideo: replaceVideo,
+  }));
+
   const screenClass = `screen-${screen}`;
 
   const DropdownMenu = (
@@ -156,11 +163,30 @@ export default (props: ScreenProps) => {
     </Menu>
   );
 
+  const MediaDom = (data: Player[]) => {
+    return data.map((item, index) => {
+      return (
+        <div
+          key={'player-content' + index}
+          className={classNames({
+            active: props.showScreen !== false && playerActive === index && !isFullscreen,
+            'full-screen': isFullscreen,
+          })}
+          onClick={() => {
+            setPlayerActive(index);
+          }}
+        >
+          <LivePlayer key={item.key} url={item.url} />
+        </div>
+      );
+    });
+  };
+
   return (
-    <div className={'live-player-warp'}>
+    <div className={classNames('live-player-warp', props.className)}>
       <div className={'live-player-content'}>
-        <div className={'player-screen-tool'}>
-          {props.showScreen !== false && (
+        {props.showScreen !== false && (
+          <div className={'player-screen-tool'}>
             <>
               <div></div>
               <div>
@@ -174,7 +200,7 @@ export default (props: ScreenProps) => {
                   value={screen}
                   onChange={(e) => {
                     if (e.target.value) {
-                      setScreen(e.target.value);
+                      screenChange(e.target.value);
                     } else {
                       // 全屏操作
                       setFull();
@@ -230,26 +256,11 @@ export default (props: ScreenProps) => {
                 </Tooltip>
               </div>
             </>
-          )}
-        </div>
+          </div>
+        )}
         <div className={'player-body'}>
           <div className={classNames('player-screen', screenClass)} ref={fullscreenRef}>
-            {players.map((player, index) => {
-              return (
-                <div
-                  key={`player_body_${index}`}
-                  className={classNames({
-                    active: props.showScreen !== false && playerActive === index && !isFullscreen,
-                    'full-screen': isFullscreen,
-                  })}
-                  onClick={() => {
-                    setPlayerActive(index);
-                  }}
-                >
-                  <LivePlayer key={player.id || `player_${index}`} url={player.url || ''} />
-                </div>
-              );
-            })}
+            {MediaDom(players)}
           </div>
         </div>
       </div>
@@ -258,13 +269,15 @@ export default (props: ScreenProps) => {
           <div
             className={'direction-item up'}
             onMouseDown={() => {
-              if (props.onMouseDown && props.id && props.channelId) {
-                props.onMouseDown(props.id, props.channelId, 'UP');
+              const { id, channelId } = players[playerActive];
+              if (id && channelId && props.onMouseDown) {
+                props.onMouseDown(id, channelId, 'UP');
               }
             }}
             onMouseUp={() => {
-              if (props.onMouseUp && props.id && props.channelId) {
-                props.onMouseUp(props.id, props.channelId, 'UP');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseUp && id && channelId) {
+                props.onMouseUp(id, channelId, 'UP');
               }
             }}
           >
@@ -273,13 +286,15 @@ export default (props: ScreenProps) => {
           <div
             className={'direction-item right'}
             onMouseDown={() => {
-              if (props.onMouseDown && props.id && props.channelId) {
-                props.onMouseDown(props.id, props.channelId, 'RIGHT');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseDown && id && channelId) {
+                props.onMouseDown(id, channelId, 'RIGHT');
               }
             }}
             onMouseUp={() => {
-              if (props.onMouseUp && props.id && props.channelId) {
-                props.onMouseUp(props.id, props.channelId, 'RIGHT');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseUp && id && channelId) {
+                props.onMouseUp(id, channelId, 'RIGHT');
               }
             }}
           >
@@ -288,13 +303,15 @@ export default (props: ScreenProps) => {
           <div
             className={'direction-item left'}
             onMouseDown={() => {
-              if (props.onMouseDown && props.id && props.channelId) {
-                props.onMouseDown(props.id, props.channelId, 'LEFT');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseDown && id && channelId) {
+                props.onMouseDown(id, channelId, 'LEFT');
               }
             }}
             onMouseUp={() => {
-              if (props.onMouseUp && props.id && props.channelId) {
-                props.onMouseUp(props.id, props.channelId, 'LEFT');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseUp && id && channelId) {
+                props.onMouseUp(id, channelId, 'LEFT');
               }
             }}
           >
@@ -303,13 +320,15 @@ export default (props: ScreenProps) => {
           <div
             className={'direction-item down'}
             onMouseDown={() => {
-              if (props.onMouseDown && props.id && props.channelId) {
-                props.onMouseDown(props.id, props.channelId, 'DOWN');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseDown && id && channelId) {
+                props.onMouseDown(id, channelId, 'DOWN');
               }
             }}
             onMouseUp={() => {
-              if (props.onMouseUp && props.id && props.channelId) {
-                props.onMouseUp(props.id, props.channelId, 'DOWN');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseUp && id && channelId) {
+                props.onMouseUp(id, channelId, 'DOWN');
               }
             }}
           >
@@ -317,16 +336,18 @@ export default (props: ScreenProps) => {
           </div>
           <div
             className={'direction-audio'}
-            onMouseDown={() => {
-              if (props.onMouseDown && props.id && props.channelId) {
-                props.onMouseDown(props.id, props.channelId, 'AUDIO');
-              }
-            }}
-            onMouseUp={() => {
-              if (props.onMouseUp && props.id && props.channelId) {
-                props.onMouseUp(props.id, props.channelId, 'AUDIO');
-              }
-            }}
+            // onMouseDown={() => {
+            //   const { id, channelId } = players[playerActive];
+            //   if (props.onMouseDown && id && channelId) {
+            //     props.onMouseDown(id, channelId, 'AUDIO');
+            //   }
+            // }}
+            // onMouseUp={() => {
+            //   const { id, channelId } = players[playerActive];
+            //   if (props.onMouseUp && id && channelId) {
+            //     props.onMouseUp(id, channelId, 'AUDIO');
+            //   }
+            // }}
           >
             {/*<AudioOutlined />*/}
           </div>
@@ -335,13 +356,15 @@ export default (props: ScreenProps) => {
           <div
             className={'zoom-item zoom-in'}
             onMouseDown={() => {
-              if (props.onMouseDown && props.id && props.channelId) {
-                props.onMouseDown(props.id, props.channelId, 'ZOOM_IN');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseDown && id && channelId) {
+                props.onMouseDown(id, channelId, 'ZOOM_IN');
               }
             }}
             onMouseUp={() => {
-              if (props.onMouseUp && props.id && props.channelId) {
-                props.onMouseUp(props.id, props.channelId, 'ZOOM_IN');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseUp && id && channelId) {
+                props.onMouseUp(id, channelId, 'ZOOM_IN');
               }
             }}
           >
@@ -350,13 +373,15 @@ export default (props: ScreenProps) => {
           <div
             className={'zoom-item zoom-out'}
             onMouseDown={() => {
-              if (props.onMouseDown && props.id && props.channelId) {
-                props.onMouseDown(props.id, props.channelId, 'ZOOM_OUT');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseDown && id && channelId) {
+                props.onMouseDown(id, channelId, 'ZOOM_OUT');
               }
             }}
             onMouseUp={() => {
-              if (props.onMouseUp && props.id && props.channelId) {
-                props.onMouseUp(props.id, props.channelId, 'ZOOM_OUT');
+              const { id, channelId } = players[playerActive];
+              if (props.onMouseUp && id && channelId) {
+                props.onMouseUp(id, channelId, 'ZOOM_OUT');
               }
             }}
           >
@@ -366,4 +391,4 @@ export default (props: ScreenProps) => {
       </div>
     </div>
   );
-};
+});

+ 6 - 2
src/components/Player/index.less

@@ -61,8 +61,12 @@
   }
 
   .live-player-tools {
-    flex-basis: 288px;
-    padding: 50px 12px 0 40px;
+    display: flex;
+    flex-basis: 250px;
+    flex-direction: column;
+    justify-content: center;
+    margin-left: 24px;
+    padding: 0 12px;
 
     .direction {
       position: relative;

+ 3 - 0
src/components/Player/index.tsx

@@ -8,6 +8,7 @@ export type PlayerProps = {
   muted?: boolean;
   poster?: string;
   timeout?: number;
+  className?: string;
   onDestroy?: () => void;
   onMessage?: (msg: any) => void;
   onError?: (err: any) => void;
@@ -18,6 +19,7 @@ export type PlayerProps = {
   onSnapOutside?: (base64: any) => void;
   onSnapInside?: (base64: any) => void;
   onCustomButtons?: (name: any) => void;
+  onClick?: () => void;
 };
 
 export default (props: PlayerProps) => {
@@ -99,6 +101,7 @@ export default (props: PlayerProps) => {
         player.current = r;
         EventInit();
       }}
+      class={props.className}
       live={'live' in props ? props.live !== false : true}
       autoplay={'autoplay' in props ? props.autoplay !== false : true}
       muted={'muted' in props ? props.muted !== false : true}

+ 0 - 50
src/pages/media/Device/Channel/Live.tsx

@@ -1,50 +0,0 @@
-// 通道直播
-import { useState } from 'react';
-import { Radio, Modal } from 'antd';
-import { ScreenPlayer } from '@/components';
-
-interface LiveProps {
-  visible: boolean;
-  url: string;
-  onCancel: () => void;
-}
-
-const LiveFC = (props: LiveProps) => {
-  const [] = useState();
-
-  return (
-    <Modal
-      maskClosable={false}
-      visible={props.visible}
-      title={'播放'}
-      width={800}
-      onCancel={() => {
-        if (props.onCancel) {
-          props.onCancel();
-        }
-      }}
-      onOk={() => {
-        if (props.onCancel) {
-          props.onCancel();
-        }
-      }}
-    >
-      <div>
-        <ScreenPlayer showScreen={false} />
-      </div>
-      <div>
-        <Radio.Group
-          optionType={'button'}
-          buttonStyle={'solid'}
-          options={[
-            { label: 'MP4', value: 'mp4' },
-            { label: 'FLV', value: 'flv' },
-            { label: 'HLS', value: 'hls' },
-          ]}
-        />
-      </div>
-    </Modal>
-  );
-};
-
-export default LiveFC;

+ 19 - 0
src/pages/media/Device/Channel/Live/index.less

@@ -0,0 +1,19 @@
+.media-live {
+  .live-player-tools {
+    flex-basis: 230px;
+
+    .direction-item {
+      font-size: 30px !important;
+    }
+
+    .zoom-item {
+      font-size: 20px !important;
+    }
+  }
+}
+
+.media-live-tool {
+  display: flex;
+  justify-content: center;
+  margin-top: 24px;
+}

+ 84 - 0
src/pages/media/Device/Channel/Live/index.tsx

@@ -0,0 +1,84 @@
+// 通道直播
+import { useCallback, useRef, useState } from 'react';
+import { Radio, Modal } from 'antd';
+import { ScreenPlayer } from '@/components';
+import { service } from '../index';
+import './index.less';
+
+interface LiveProps {
+  visible: boolean;
+  deviceId: string;
+  channelId: string;
+  onCancel: () => void;
+}
+
+const LiveFC = (props: LiveProps) => {
+  const [mediaType, setMediaType] = useState('mp4');
+  const player = useRef<any>(null);
+
+  const mediaStart = useCallback(
+    async (type) => {
+      const _url = service.ptzStart(props.deviceId, props.channelId, type);
+      player.current?.replaceVideo(props.deviceId, props.channelId, _url);
+    },
+    [props.channelId, props.deviceId],
+  );
+
+  return (
+    <Modal
+      destroyOnClose
+      maskClosable={false}
+      visible={props.visible}
+      title={'播放'}
+      width={800}
+      onCancel={() => {
+        if (props.onCancel) {
+          props.onCancel();
+        }
+      }}
+      onOk={() => {
+        if (props.onCancel) {
+          props.onCancel();
+        }
+      }}
+    >
+      <div className={'media-live'}>
+        {props.visible && (
+          <ScreenPlayer
+            id={props.deviceId}
+            channelId={props.channelId}
+            ref={(ref) => {
+              player.current = ref;
+              mediaStart('mp4');
+            }}
+            showScreen={false}
+            onMouseUp={(id, cId) => {
+              service.ptzStop(id, cId);
+            }}
+            onMouseDown={(id, cId, type) => {
+              service.ptzTool(id, cId, type);
+            }}
+          />
+        )}
+      </div>
+      <div className={'media-live-tool'}>
+        <Radio.Group
+          optionType={'button'}
+          buttonStyle={'solid'}
+          value={mediaType}
+          onChange={(e) => {
+            setMediaType(e.target.value);
+            mediaStart(e.target.value);
+          }}
+          options={[
+            { label: 'MP4', value: 'mp4' },
+            { label: 'FLV', value: 'flv' },
+            { label: 'HLS', value: 'hls' },
+          ]}
+        />
+      </div>
+    </Modal>
+  );
+};
+
+export default LiveFC;

+ 2 - 1
src/pages/media/Device/Channel/Tree/index.tsx

@@ -15,8 +15,9 @@ export default (props: TreeProps) => {
   const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(['']);
   const { run: getTreeData } = useRequest(service.queryTree, {
     manual: true,
+    formatResult: (res) => res.result,
     onSuccess: (res) => {
-      treeData[0].children = res.result;
+      treeData[0].children = res.result || [];
       setTreeData(treeData);
     },
   });

+ 36 - 27
src/pages/media/Device/Channel/index.tsx

@@ -33,6 +33,7 @@ export default () => {
   const [liveVisible, setLiveVisible] = useState(false);
   const [current, setCurrent] = useState<ChannelItem>();
   const [deviceId, setDeviceId] = useState('');
+  const [channelId, setChannelId] = useState('');
   const [type, setType] = useState('');
 
   const location = useLocation();
@@ -139,6 +140,7 @@ export default () => {
         <Tooltip key={'live'} title={'播发'}>
           <a
             onClick={() => {
+              setChannelId(record.channelId);
               setLiveVisible(true);
             }}
           >
@@ -149,7 +151,9 @@ export default () => {
           <a
             onClick={() => {
               history.push(
-                `${getMenuPathByCode(MENUS_CODE['media/Device/Playback'])}?id=${record.channelId}`,
+                `${getMenuPathByCode(MENUS_CODE['media/Device/Playback'])}?id=${
+                  record.channelId
+                }&channelId=${record.channelId}`,
               );
             }}
           >
@@ -181,25 +185,27 @@ export default () => {
   return (
     <PageContainer>
       <div className={'device-channel-warp'}>
-        <div className={'left'}>
-          <Tree
-            deviceId={deviceId}
-            onSelect={(key) => {
-              if (key === deviceId && actionRef.current?.reset) {
-                actionRef.current?.reset();
-              } else {
-                setQueryParam({
-                  terms: [
-                    {
-                      column: 'parentId',
-                      value: key,
-                    },
-                  ],
-                });
-              }
-            }}
-          />
-        </div>
+        {type === ProviderValue.GB281 && (
+          <div className={'left'}>
+            <Tree
+              deviceId={deviceId}
+              onSelect={(key) => {
+                if (key === deviceId && actionRef.current?.reset) {
+                  actionRef.current?.reset();
+                } else {
+                  setQueryParam({
+                    terms: [
+                      {
+                        column: 'parentId',
+                        value: key,
+                      },
+                    ],
+                  });
+                }
+              }}
+            />
+          </div>
+        )}
         <div className={'right'}>
           <SearchComponent field={columns} onSearch={searchFn} />
           <ProTable<ChannelItem>
@@ -262,13 +268,16 @@ export default () => {
           actionRef.current?.reload();
         }}
       />
-      <Live
-        visible={liveVisible}
-        url={''}
-        onCancel={() => {
-          setLiveVisible(false);
-        }}
-      />
+      {liveVisible && (
+        <Live
+          visible={liveVisible}
+          deviceId={deviceId}
+          channelId={channelId}
+          onCancel={() => {
+            setLiveVisible(false);
+          }}
+        />
+      )}
     </PageContainer>
   );
 };

+ 56 - 0
src/pages/media/Device/Channel/service.ts

@@ -1,6 +1,7 @@
 import BaseService from '@/utils/BaseService';
 import { request } from 'umi';
 import type { ChannelItem } from './typings';
+import Token from '@/utils/token';
 
 class Service extends BaseService<ChannelItem> {
   //
@@ -20,6 +21,61 @@ class Service extends BaseService<ChannelItem> {
 
   // 设备详情
   deviceDetail = (id: string) => request(`${this.uri}/device/${id}`, { method: 'GET' });
+
+  // 开始直播
+  ptzStart = (deviceId: string, channelId: string, type: string) =>
+    `${this.uri}/device/${deviceId}/${channelId}/live.${type}?:X_Access_Token=${Token.get()}`;
+
+  // 云台控制-停止
+  ptzStop = (deviceId: string, channelId: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/_stop`, { method: 'POST' });
+
+  // 云台控制-缩放、转向等
+  ptzTool = (deviceId: string, channelId: string, direct: string, speed: number = 90) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/_ptz/${direct}/${speed}`, {
+      method: 'POST',
+    });
+
+  // 查询是否正在录像
+  ptzIsRecord = (deviceId: string, channelId: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/live/recording`, { method: 'GET' });
+
+  // 开始录像
+  recordStart = (deviceId: string, channelId: string, data: any) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/_record`, { method: 'POST', data });
+
+  // 停止录像
+  recordStop = (deviceId: string, channelId: string, data: any) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/_stop-record`, { method: 'POST', data });
+
+  // 查询本地回放记录
+  queryRecordLocal = (deviceId: string, channelId: string, data: any) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/records/in-local`, {
+      method: 'POST',
+      data,
+    });
+
+  // 播放本地回放
+  playbackLocal = (deviceId: string, channelId: string, suffix: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/playback.${suffix}`, { method: 'GET' });
+
+  // 本地录像播放控制
+  playbackControl = (deviceId: string, channelId: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/stream-control`, { method: 'POST' });
+
+  // 查询云端回放记录
+  recordsInServer = (deviceId: string, channelId: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/records/in-server`, { method: 'POST' });
+
+  // 查询云端回放文件信息
+  recordsInServerFiles = (deviceId: string, channelId: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/records/in-server/files`, {
+      method: 'POST',
+    });
+
+  // 播放云端回放
+  playbackStart = (recordId: string) =>
+    request(`${this.uri}/record/${recordId}.mp4`, { method: 'GET' });
 }
 
 export default Service;

+ 49 - 0
src/pages/media/Device/Playback/index.less

@@ -0,0 +1,49 @@
+@borderColor: #d9d9d9;
+
+.playback-warp {
+  display: flex;
+  padding: 24px;
+  background-color: #fff;
+
+  .playback-left {
+    display: flex;
+    flex-grow: 1;
+    width: 0;
+
+    .playback-media {
+      width: 100%;
+    }
+  }
+
+  .playback-right {
+    width: 280px;
+    margin-left: 24px;
+
+    .playback-calendar {
+      margin-top: 16px;
+      border: 1px solid @borderColor;
+      border-radius: 2px;
+
+      .ant-picker-calendar-header {
+        justify-content: space-between;
+
+        > div:nth-child(3) {
+          display: none;
+        }
+      }
+    }
+
+    .playback-list {
+      display: flex;
+      height: 300px;
+      margin-top: 16px;
+      overflow-y: auto;
+      border: 1px solid @borderColor;
+
+      &.no-list {
+        align-items: center;
+        justify-content: center;
+      }
+    }
+  }
+}

+ 121 - 1
src/pages/media/Device/Playback/index.tsx

@@ -1,6 +1,126 @@
 // 回放
 import { PageContainer } from '@ant-design/pro-layout';
+import LivePlayer from '@/components/Player';
+import { useCallback, useEffect, useState } from 'react';
+import { Select, Calendar, Empty, List } from 'antd';
+import { useLocation } from 'umi';
+import Service from './service';
+import './index.less';
+import { recordsItemType } from '@/pages/media/Device/Playback/typings';
+import * as moment from 'moment';
+import classNames from 'classnames';
+
+const service = new Service('media');
 
 export default () => {
-  return <PageContainer>回放</PageContainer>;
+  const [url] = useState('');
+  const [type, setType] = useState('local');
+  const [historyList, setHistoryList] = useState<recordsItemType[]>([]);
+  const [time, setTime] = useState<any>('');
+  const location = useLocation();
+
+  const param = new URLSearchParams(location.search);
+  const deviceId = param.get('id');
+  const channelId = param.get('channelId');
+
+  const queryLocalRecords = useCallback(
+    async (date: any) => {
+      if (deviceId && channelId && date) {
+        const params = {
+          startTime: date.format('YYYY-MM-DD 00:00:00'),
+          endTime: date.format('YYYY-MM-DD 23:59:59'),
+        };
+        let list: recordsItemType[] = [];
+        const localResp = await service.queryRecordLocal(deviceId, channelId, params);
+
+        if (localResp.status === 200) {
+          list = [localResp.result];
+        }
+
+        const serviceResp = await service.recordsInServer(deviceId, channelId, {
+          ...params,
+          includeFiles: false,
+        });
+
+        if (serviceResp.status === 200) {
+          list = [...list, ...serviceResp.result];
+        }
+
+        setHistoryList(list);
+      }
+    },
+    [time],
+  );
+
+  useEffect(() => {
+    setTime(moment(new Date()));
+    queryLocalRecords(moment(new Date()));
+  }, []);
+
+  return (
+    <PageContainer>
+      <div className={'playback-warp'}>
+        <div className={'playback-left'}>
+          <LivePlayer url={url} className={'playback-media'} />
+        </div>
+        <div className={'playback-right'}>
+          <Select
+            value={type}
+            options={[
+              { label: '云端', value: 'cloud' },
+              { label: '本地', value: 'local' },
+            ]}
+            style={{ width: '100%' }}
+            onSelect={(key: string) => {
+              setType(key);
+            }}
+          />
+          <div className={'playback-calendar'}>
+            <Calendar
+              value={time}
+              onChange={(date) => {
+                setTime(date);
+                queryLocalRecords(date);
+              }}
+              disabledDate={(currentDate) => currentDate > moment(new Date())}
+              fullscreen={false}
+            />
+          </div>
+          <div className={classNames('playback-list', { 'no-list': !!historyList.length })}>
+            {historyList && historyList.length ? (
+              <List
+                itemLayout="horizontal"
+                dataSource={historyList}
+                renderItem={(item) => {
+                  const startTime = moment(item.startTime);
+                  const startH = startTime.hours();
+                  const startM = startTime.minutes();
+                  const srattD = startTime.date();
+
+                  const endTime = moment(item.endTime);
+                  const endH = endTime.hours();
+                  const endM = endTime.minutes();
+                  const endD = endTime.date();
+                  return (
+                    <List.Item
+                      actions={[
+                        <a key="list-loadmore-edit">edit</a>,
+                        <a key="list-loadmore-more">more</a>,
+                      ]}
+                    >
+                      {`${startH}-${startM}-${srattD}`} ~ {`${endH}-${endM}-${endD}`}
+                    </List.Item>
+                  );
+                }}
+              >
+                <div></div>
+              </List>
+            ) : (
+              <Empty />
+            )}
+          </div>
+        </div>
+      </div>
+    </PageContainer>
+  );
 };

+ 44 - 0
src/pages/media/Device/Playback/service.ts

@@ -0,0 +1,44 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import Token from '@/utils/token';
+import { recordsItemType } from '@/pages/media/Device/Playback/typings';
+
+class Service extends BaseService<recordsItemType> {
+  // 开始直播
+  ptzStart = (deviceId: string, channelId: string, type: string) =>
+    `${this.uri}/device/${deviceId}/${channelId}/live.${type}?:X_Access_Token=${Token.get()}`;
+
+  // 查询本地回放记录
+  queryRecordLocal = (deviceId: string, channelId: string, data?: any) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/records/in-local`, {
+      method: 'POST',
+      data,
+    });
+
+  // 播放本地回放
+  playbackLocal = (deviceId: string, channelId: string, suffix: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/playback.${suffix}`, { method: 'GET' });
+
+  // 本地录像播放控制
+  playbackControl = (deviceId: string, channelId: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/stream-control`, { method: 'POST' });
+
+  // 查询云端回放记录
+  recordsInServer = (deviceId: string, channelId: string, data: any) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/records/in-server`, {
+      method: 'POST',
+      data,
+    });
+
+  // 查询云端回放文件信息
+  recordsInServerFiles = (deviceId: string, channelId: string) =>
+    request(`${this.uri}/device/${deviceId}/${channelId}/records/in-server/files`, {
+      method: 'POST',
+    });
+
+  // 播放云端回放
+  playbackStart = (recordId: string) =>
+    request(`${this.uri}/record/${recordId}.mp4`, { method: 'GET' });
+}
+
+export default Service;

+ 10 - 0
src/pages/media/Device/Playback/typings.d.ts

@@ -0,0 +1,10 @@
+export type recordsItemType = {
+  channelId: string;
+  deviceId: string;
+  endTime: number;
+  fileSize: number;
+  name: string;
+  secrecy: string;
+  startTime: number;
+  type: string;
+};

+ 9 - 13
src/pages/media/SplitScreen/index.tsx

@@ -1,27 +1,23 @@
 // 视频分屏
 import { PageContainer } from '@ant-design/pro-layout';
-import { Card, message } from 'antd';
+import { Card } from 'antd';
 import LeftTree from './tree';
 import { ScreenPlayer } from '@/components';
 import { ptzStop, ptzTool } from './service';
-import { useState } from 'react';
+import { useRef, useState } from 'react';
 import { ptzStart } from './service';
 import './index.less';
 
 const SplitScreen = () => {
   const [deviceId, setDeviceId] = useState('');
   const [channelId, setChannelId] = useState('');
-  const [url, setUrl] = useState('');
+  const player = useRef<any>(null);
 
   const mediaStart = async (dId: string, cId: string) => {
-    setChannelId(dId);
-    setDeviceId(cId);
-    const resp = await ptzStart(dId, cId);
-    if (resp.status === 200) {
-      setUrl(resp.result.mp4);
-    } else {
-      message.error(resp.result);
-    }
+    setChannelId(cId);
+    setDeviceId(dId);
+    const url = ptzStart(dId, cId, 'mp4');
+    player.current?.replaceVideo(dId, cId, url);
   };
 
   return (
@@ -31,14 +27,14 @@ const SplitScreen = () => {
           <LeftTree onSelect={mediaStart} />
           <div className="right-content">
             <ScreenPlayer
+              ref={player}
               id={deviceId}
-              url={url}
               channelId={channelId}
               onMouseUp={(id, cId) => {
                 ptzStop(id, cId);
               }}
               onMouseDown={(id, cId, type) => {
-                ptzTool(id, cId, type, 90);
+                ptzTool(id, cId, type);
               }}
             />
           </div>

+ 5 - 4
src/pages/media/SplitScreen/service.ts

@@ -1,5 +1,6 @@
 import { request } from '@@/plugin-request/request';
 import SystemConst from '@/utils/const';
+import Token from '@/utils/token';
 
 const url = `/${SystemConst.API_BASE}/media`;
 
@@ -8,16 +9,16 @@ export const getMediaTree = (data?: any) =>
   request(`${url}/device/_query/no-paging`, { method: 'POST', data });
 
 // 开始直播
-export const ptzStart = (deviceId: string, channelId: string) =>
-  request(`${url}/device/${deviceId}/${channelId}/_start`, { method: 'POST' });
+export const ptzStart = (deviceId: string, channelId: string, type: string) =>
+  `${url}/device/${deviceId}/${channelId}/live.${type}?:X_Access_Token=${Token.get()}`;
 
 // 云台控制-停止
 export const ptzStop = (deviceId: string, channelId: string) =>
-  request(`${url}/device/${deviceId}/${channelId}/_pzt/STOP`, { method: 'POST' });
+  request(`${url}/device/${deviceId}/${channelId}/_stop`, { method: 'POST' });
 
 // 云台控制-缩放、转向等
 export const ptzTool = (deviceId: string, channelId: string, direct: string, speed: number = 90) =>
-  request(`${url}/device/${deviceId}/${channelId}/_pzt/${direct}/${speed}`, { method: 'POST' });
+  request(`${url}/device/${deviceId}/${channelId}/_ptz/${direct}/${speed}`, { method: 'POST' });
 
 // 查询设备通道列表
 export const queryChannel = (data: any) =>

+ 1 - 1
src/pages/system/Menu/Detail/buttons.tsx

@@ -25,7 +25,7 @@ export default (props: ButtonsProps) => {
 
   const { data: permissions, run: queryPermissions } = useRequest(service.queryPermission, {
     manual: true,
-    formatResult: (response) => response.result.data,
+    formatResult: (response) => response.result,
   });
 
   useEffect(() => {

+ 4 - 4
src/pages/system/Menu/Detail/edit.tsx

@@ -33,7 +33,7 @@ type EditProps = {
 export default (props: EditProps) => {
   const intl = useIntl();
   const [disabled, setDisabled] = useState(true);
-  const [show, setShow] = useState(true);
+  const [show] = useState(true);
   const [accessSupport, setAccessSupport] = useState('unsupported');
   const history = useHistory();
 
@@ -118,9 +118,9 @@ export default (props: EditProps) => {
     }
     setDisabled(!!props.data.id);
 
-    if (props.data.options) {
-      setShow(props.data.options.switch);
-    }
+    // if (props.data.options) {
+    //   setShow(props.data.options.switch);
+    // }
     /* eslint-disable */
   }, [props.data]);