@vtx/player 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +11 -0
- package/.eslintrc.js +31 -0
- package/.prettierignore +10 -0
- package/.prettierrc.js +22 -0
- package/.storybook/main.js +58 -0
- package/.storybook/manager.js +7 -0
- package/.storybook/preview-head.html +5 -0
- package/.storybook/preview.js +9 -0
- package/.storybook/proxy.js +6 -0
- package/.storybook/theme.js +8 -0
- package/.stylelintrc.json +13 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +16 -0
- package/README.md +3 -0
- package/commitlint.config.js +10 -0
- package/package.json +98 -0
- package/public/h5player.min.js +311 -0
- package/public/playctrl1/AudioRenderer.js +225 -0
- package/public/playctrl1/DecodeWorker.js +711 -0
- package/public/playctrl1/Decoder.js +1 -0
- package/public/playctrl1/SuperRender_10.js +396 -0
- package/public/playctrl2/Decoder.js +21 -0
- package/public/playctrl2/Decoder.wasm +0 -0
- package/public/playctrl2/Decoder.worker.js +1 -0
- package/public/playctrl3/Decoder.js +21 -0
- package/public/playctrl3/Decoder.wasm +0 -0
- package/public/playctrl3/Decoder.worker.js +1 -0
- package/rollup.config.js +52 -0
- package/src/api/fetch.ts +50 -0
- package/src/api/index.ts +44 -0
- package/src/api/types.ts +141 -0
- package/src/components/bill-player/index.less +58 -0
- package/src/components/bill-player/index.stories.mdx +24 -0
- package/src/components/bill-player/index.stories.tsx +14 -0
- package/src/components/bill-player/index.tsx +269 -0
- package/src/components/controls/images/arrow.png +0 -0
- package/src/components/controls/images/error.png +0 -0
- package/src/components/controls/index.less +182 -0
- package/src/components/controls/index.tsx +312 -0
- package/src/components/history-control/images/collapse.png +0 -0
- package/src/components/history-control/index.less +211 -0
- package/src/components/history-control/index.stories.mdx +64 -0
- package/src/components/history-control/index.stories.tsx +140 -0
- package/src/components/history-control/index.tsx +344 -0
- package/src/components/history-player/index.less +98 -0
- package/src/components/history-player/index.stories.mdx +38 -0
- package/src/components/history-player/index.stories.tsx +12 -0
- package/src/components/history-player/index.tsx +205 -0
- package/src/components/live-channel-player/index.stories.mdx +29 -0
- package/src/components/live-channel-player/index.stories.tsx +11 -0
- package/src/components/live-channel-player/index.tsx +79 -0
- package/src/components/live-control/index.less +65 -0
- package/src/components/live-control/index.stories.mdx +66 -0
- package/src/components/live-control/index.stories.tsx +69 -0
- package/src/components/live-control/index.tsx +255 -0
- package/src/components/live-player/index.less +71 -0
- package/src/components/live-player/index.stories.mdx +35 -0
- package/src/components/live-player/index.stories.tsx +12 -0
- package/src/components/live-player/index.tsx +118 -0
- package/src/components/player/index.ts +293 -0
- package/src/context/index.ts +13 -0
- package/src/hooks/useSettings.tsx +14 -0
- package/src/icons/index.less +27 -0
- package/src/icons/index.tsx +518 -0
- package/src/main.ts +9 -0
- package/src/stories/intro.stories.mdx +16 -0
- package/src/typings/@vtx/utils/index.d.ts +27 -0
- package/src/typings/h5player.d.ts +133 -0
- package/src/utils/index.ts +177 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import HistoryControl from '.';
|
|
2
|
+
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
|
|
3
|
+
|
|
4
|
+
import * as stories from './index.stories';
|
|
5
|
+
|
|
6
|
+
<Meta title="组件/历史视频控制器" />
|
|
7
|
+
|
|
8
|
+
# 历史视频控制器
|
|
9
|
+
|
|
10
|
+
## 引入
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { HistoryControl } from '@vtx/player';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 方法调用
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { HistoryControl } from '@vtx/player';
|
|
20
|
+
|
|
21
|
+
const MyApp = () => {
|
|
22
|
+
const ref = useRef<HistoryControlHandle>(null);
|
|
23
|
+
const params = {};
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<button onClick={() => ref.load(params)}>加载视频</button>
|
|
27
|
+
<HistoryControl ref={ref} />
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 方法类型
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
type HistoryControlHandle = {
|
|
37
|
+
load: (channel: ChannelType) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ChannelType {
|
|
41
|
+
/** 租户ID */
|
|
42
|
+
tenantId: string;
|
|
43
|
+
/** 频道号 */
|
|
44
|
+
channelNum: string;
|
|
45
|
+
/** 设备ID */
|
|
46
|
+
deviceId: string;
|
|
47
|
+
/** 协议 */
|
|
48
|
+
protocol?: string;
|
|
49
|
+
/** 开始时间,格式:2022-06-01 00:00:00 */
|
|
50
|
+
beginTime: string;
|
|
51
|
+
/** 结束时间,格式:2022-06-01 23:59:59 */
|
|
52
|
+
endTime: string;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 属性
|
|
57
|
+
|
|
58
|
+
<ArgsTable of={HistoryControl} />
|
|
59
|
+
|
|
60
|
+
## 例子
|
|
61
|
+
|
|
62
|
+
<Canvas>
|
|
63
|
+
<Story name="历史视频控制器" story={stories.Default} />
|
|
64
|
+
</Canvas>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import HistoryControl, { HistoryControlHandle } from '.';
|
|
4
|
+
import { ComponentStory } from '@storybook/react';
|
|
5
|
+
import { Button, DatePicker, Form, Input, Space } from 'antd';
|
|
6
|
+
import { useForm } from 'antd/lib/form/Form';
|
|
7
|
+
import moment from 'moment';
|
|
8
|
+
|
|
9
|
+
type FormData = {
|
|
10
|
+
/** 租户ID */
|
|
11
|
+
tenantId: string;
|
|
12
|
+
/** 频道号 */
|
|
13
|
+
channelNum: string;
|
|
14
|
+
/** 设备ID */
|
|
15
|
+
deviceId: string;
|
|
16
|
+
/** 协议 */
|
|
17
|
+
protocol?: string;
|
|
18
|
+
/** 开始时间,格式:2022-06-01 00:00:00 */
|
|
19
|
+
beginTime: string;
|
|
20
|
+
/** 结束时间,格式:2022-06-01 23:59:59 */
|
|
21
|
+
endTime: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const Template: ComponentStory<typeof HistoryControl> = (args) => {
|
|
25
|
+
const ref = useRef<HistoryControlHandle>(null);
|
|
26
|
+
const [form] = useForm();
|
|
27
|
+
const onFinish = (values: FormData) => {
|
|
28
|
+
const control = ref.current;
|
|
29
|
+
if (control)
|
|
30
|
+
control.load({
|
|
31
|
+
...values,
|
|
32
|
+
beginTime: moment(values.beginTime).format('YYYY-MM-DD HH:mm:ss'),
|
|
33
|
+
endTime: moment(values.endTime).format('YYYY-MM-DD HH:mm:ss'),
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
return (
|
|
37
|
+
<Space size={24}>
|
|
38
|
+
<HistoryControl {...args} ref={ref} />
|
|
39
|
+
<div>
|
|
40
|
+
<Space style={{ marginBottom: 20 }}>
|
|
41
|
+
<Button
|
|
42
|
+
onClick={() => {
|
|
43
|
+
form.setFieldsValue({
|
|
44
|
+
channelNum: '98956603975e480c8d673c87f1126f5f',
|
|
45
|
+
deviceId: 'c15f2219af0d4770b399dc5e519fbd37',
|
|
46
|
+
});
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
海康测试-万科中粮
|
|
50
|
+
</Button>
|
|
51
|
+
<Button
|
|
52
|
+
onClick={() => {
|
|
53
|
+
form.setFieldsValue({
|
|
54
|
+
channelNum: '396ee1a6d586404982ce4c412bdcaf49',
|
|
55
|
+
deviceId: '25d0b7c5e1624cf0a9abf18c45cca7a0',
|
|
56
|
+
});
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
FLV测试-惠盛
|
|
60
|
+
</Button>
|
|
61
|
+
<Button
|
|
62
|
+
onClick={() => {
|
|
63
|
+
form.setFieldsValue({
|
|
64
|
+
channelNum: '87cc1a9de57a41e19344d4d3e75a5976',
|
|
65
|
+
deviceId: '687a1c4765c0453ea58618e5c7c6d9dd',
|
|
66
|
+
});
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
海康测试-垃圾房对讲
|
|
70
|
+
</Button>
|
|
71
|
+
</Space>
|
|
72
|
+
<Form
|
|
73
|
+
form={form}
|
|
74
|
+
name="basic"
|
|
75
|
+
labelCol={{ span: 8 }}
|
|
76
|
+
wrapperCol={{ span: 16 }}
|
|
77
|
+
onFinish={onFinish}
|
|
78
|
+
initialValues={{
|
|
79
|
+
channelNum: '98956603975e480c8d673c87f1126f5f',
|
|
80
|
+
deviceId: 'c15f2219af0d4770b399dc5e519fbd37',
|
|
81
|
+
tenantId: '60edef2faea44a39a6385146f4fa131c',
|
|
82
|
+
beginTime: moment().subtract(1, 'day').startOf('day'),
|
|
83
|
+
endTime: moment().subtract(1, 'day').endOf('day'),
|
|
84
|
+
}}
|
|
85
|
+
autoComplete="off"
|
|
86
|
+
>
|
|
87
|
+
<Form.Item
|
|
88
|
+
label="ChannelNum"
|
|
89
|
+
name="channelNum"
|
|
90
|
+
rules={[{ required: true, message: '请输入通道编号' }]}
|
|
91
|
+
>
|
|
92
|
+
<Input />
|
|
93
|
+
</Form.Item>
|
|
94
|
+
<Form.Item
|
|
95
|
+
label="TenantId"
|
|
96
|
+
name="tenantId"
|
|
97
|
+
rules={[{ required: true, message: '请输入通租户ID' }]}
|
|
98
|
+
>
|
|
99
|
+
<Input />
|
|
100
|
+
</Form.Item>
|
|
101
|
+
<Form.Item
|
|
102
|
+
label="DeviceId"
|
|
103
|
+
name="deviceId"
|
|
104
|
+
rules={[{ required: true, message: '请输入设备ID' }]}
|
|
105
|
+
>
|
|
106
|
+
<Input />
|
|
107
|
+
</Form.Item>
|
|
108
|
+
<Form.Item label="Protocol" name="protocol">
|
|
109
|
+
<Input />
|
|
110
|
+
</Form.Item>
|
|
111
|
+
<Form.Item
|
|
112
|
+
label="BeginTime"
|
|
113
|
+
name="beginTime"
|
|
114
|
+
rules={[{ required: true, message: '请输入开始时间' }]}
|
|
115
|
+
>
|
|
116
|
+
<DatePicker showTime />
|
|
117
|
+
</Form.Item>
|
|
118
|
+
<Form.Item
|
|
119
|
+
label="EndTime"
|
|
120
|
+
name="endTime"
|
|
121
|
+
rules={[{ required: true, message: '请输入结束时间' }]}
|
|
122
|
+
>
|
|
123
|
+
<DatePicker showTime />
|
|
124
|
+
</Form.Item>
|
|
125
|
+
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
|
|
126
|
+
<Button type="primary" htmlType="submit">
|
|
127
|
+
加载
|
|
128
|
+
</Button>
|
|
129
|
+
</Form.Item>
|
|
130
|
+
</Form>
|
|
131
|
+
</div>
|
|
132
|
+
</Space>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
export const Default = Template.bind({});
|
|
136
|
+
Default.args = {
|
|
137
|
+
width: 1000,
|
|
138
|
+
height: 600,
|
|
139
|
+
};
|
|
140
|
+
export default Default;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import './index.less';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
|
4
|
+
import useCollapse from 'react-collapsed';
|
|
5
|
+
|
|
6
|
+
import HistoryPlayer from '../history-player';
|
|
7
|
+
import { useRequest, useSetState } from 'ahooks';
|
|
8
|
+
import { Button, Space, Table, Tooltip } from 'antd';
|
|
9
|
+
import { ColumnsType } from 'antd/lib/table';
|
|
10
|
+
import moment from 'moment';
|
|
11
|
+
|
|
12
|
+
import { getHistoryUrl } from '@/api';
|
|
13
|
+
import { GetHistoryUrlParams, HistoryUrlItem } from '@/api/types';
|
|
14
|
+
import { PlayerHeader } from '@/components/controls';
|
|
15
|
+
import { ExpandIcon, FoldIcon, TimePositionIcon, VideoCloseIcon, VideoPlayIcon } from '@/icons';
|
|
16
|
+
import { getUrlTableData } from '@/utils';
|
|
17
|
+
|
|
18
|
+
import collapseBtnImg from './images/collapse.png';
|
|
19
|
+
|
|
20
|
+
export type HistoryControlHandle = {
|
|
21
|
+
load: (channel: GetHistoryUrlParams) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface HistoryControlProps {
|
|
25
|
+
/** 宽度 */
|
|
26
|
+
width?: number;
|
|
27
|
+
/** 高度 */
|
|
28
|
+
height?: number;
|
|
29
|
+
/** 展开/收缩按钮回调 */
|
|
30
|
+
onExpandChange?: (expand: boolean) => void;
|
|
31
|
+
/** 播放器进度回调,time:YYYY-MM-DD HH:mm:ss */
|
|
32
|
+
onTimeChange?: (time: string) => void;
|
|
33
|
+
}
|
|
34
|
+
/** 播放器参数 */
|
|
35
|
+
type PlayParams = {
|
|
36
|
+
index: number;
|
|
37
|
+
url: string;
|
|
38
|
+
startTime: string;
|
|
39
|
+
endTime: string;
|
|
40
|
+
};
|
|
41
|
+
/** 默认播放器参数 */
|
|
42
|
+
const DEFAULT_PLAY_PARAMS: PlayParams = {
|
|
43
|
+
index: -1,
|
|
44
|
+
url: '',
|
|
45
|
+
startTime: '',
|
|
46
|
+
endTime: '',
|
|
47
|
+
};
|
|
48
|
+
type State = {
|
|
49
|
+
/** 展开/收缩 */
|
|
50
|
+
expand: boolean;
|
|
51
|
+
/** 当前时间刻度位置 */
|
|
52
|
+
timeLinePosition: number;
|
|
53
|
+
/** 名称 */
|
|
54
|
+
name: string;
|
|
55
|
+
/** 表格数据 */
|
|
56
|
+
tableData: UrlsTableItemType[];
|
|
57
|
+
/** 分段数据 */
|
|
58
|
+
segmentData: GetSegmentsProps;
|
|
59
|
+
/** 通道信息 */
|
|
60
|
+
channel?: GetHistoryUrlParams;
|
|
61
|
+
/** 总地址 */
|
|
62
|
+
totalUrl: string;
|
|
63
|
+
/** 播放器参数 */
|
|
64
|
+
playParams: PlayParams;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* 历史视频控制器
|
|
68
|
+
*/
|
|
69
|
+
export const HistoryControl = forwardRef<HistoryControlHandle, HistoryControlProps>(
|
|
70
|
+
(props, ref) => {
|
|
71
|
+
const { width = '100%', height = '100%', onExpandChange, onTimeChange } = props;
|
|
72
|
+
const { runAsync } = useRequest(getHistoryUrl, {
|
|
73
|
+
manual: true,
|
|
74
|
+
});
|
|
75
|
+
const { getCollapseProps, getToggleProps, isExpanded } = useCollapse();
|
|
76
|
+
const [state, setState] = useSetState<State>({
|
|
77
|
+
expand: true,
|
|
78
|
+
name: '',
|
|
79
|
+
tableData: [],
|
|
80
|
+
segmentData: { list: [] },
|
|
81
|
+
totalUrl: '',
|
|
82
|
+
timeLinePosition: -1, //初始化,负数不显示
|
|
83
|
+
playParams: DEFAULT_PLAY_PARAMS,
|
|
84
|
+
});
|
|
85
|
+
useImperativeHandle(ref, () => {
|
|
86
|
+
return {
|
|
87
|
+
load: (channel) => {
|
|
88
|
+
if (JSON.stringify(channel) !== JSON.stringify(state.channel)) {
|
|
89
|
+
setState({ channel, timeLinePosition: -1 });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (state.channel) {
|
|
96
|
+
runAsync(state.channel).then((result) => {
|
|
97
|
+
const tableData = getUrlTableData(result);
|
|
98
|
+
const segmentData = {
|
|
99
|
+
list: result.list,
|
|
100
|
+
start: moment(state.channel?.beginTime).startOf('day'),
|
|
101
|
+
end: moment(state.channel?.endTime).endOf('day'),
|
|
102
|
+
};
|
|
103
|
+
setState({
|
|
104
|
+
tableData,
|
|
105
|
+
segmentData,
|
|
106
|
+
name: `${result.deviceName}-${result.channelName}`,
|
|
107
|
+
totalUrl: result.totalTimeUrl,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
return () => {
|
|
111
|
+
setState({
|
|
112
|
+
tableData: [],
|
|
113
|
+
segmentData: { list: [] },
|
|
114
|
+
name: ``,
|
|
115
|
+
playParams: DEFAULT_PLAY_PARAMS,
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}, [state.channel]);
|
|
120
|
+
const handleExpand = () => {
|
|
121
|
+
onExpandChange && onExpandChange(!state.expand);
|
|
122
|
+
setState({ expand: !state.expand });
|
|
123
|
+
};
|
|
124
|
+
const handleClose = () => {
|
|
125
|
+
setState({ channel: undefined, timeLinePosition: -1 });
|
|
126
|
+
};
|
|
127
|
+
const columns: ColumnsType<UrlsTableItemType> = [
|
|
128
|
+
{
|
|
129
|
+
title: '序号',
|
|
130
|
+
dataIndex: 'index',
|
|
131
|
+
width: 60,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
title: '播放视频',
|
|
135
|
+
key: 'play',
|
|
136
|
+
width: 90,
|
|
137
|
+
render: (_, record, index) => {
|
|
138
|
+
const active = state.playParams.index === index;
|
|
139
|
+
return (
|
|
140
|
+
<Button
|
|
141
|
+
className={`play-btn ${active ? 'active' : ''}`}
|
|
142
|
+
onClick={() => {
|
|
143
|
+
if (active) {
|
|
144
|
+
setState({
|
|
145
|
+
playParams: DEFAULT_PLAY_PARAMS,
|
|
146
|
+
timeLinePosition: -1,
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
const url = record.url || state.totalUrl;
|
|
150
|
+
setState({
|
|
151
|
+
playParams: {
|
|
152
|
+
index,
|
|
153
|
+
url,
|
|
154
|
+
startTime: record.beginTime,
|
|
155
|
+
endTime: record.endTime,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{active ? (
|
|
162
|
+
<>
|
|
163
|
+
<VideoCloseIcon active={true} />
|
|
164
|
+
关闭
|
|
165
|
+
</>
|
|
166
|
+
) : (
|
|
167
|
+
<>
|
|
168
|
+
<VideoPlayIcon />
|
|
169
|
+
播放
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
</Button>
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
// {
|
|
177
|
+
// title: '设备名称',
|
|
178
|
+
// dataIndex: 'deviceName',
|
|
179
|
+
// },
|
|
180
|
+
// {
|
|
181
|
+
// title: '通道名称',
|
|
182
|
+
// dataIndex: 'channelName',
|
|
183
|
+
// },
|
|
184
|
+
{
|
|
185
|
+
title: '开始时间',
|
|
186
|
+
dataIndex: 'beginTime',
|
|
187
|
+
width: 170,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
title: '结束时间',
|
|
191
|
+
dataIndex: 'endTime',
|
|
192
|
+
width: 170,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
title: '持续时间',
|
|
196
|
+
width: 120,
|
|
197
|
+
dataIndex: 'duration',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
title: '视频大小',
|
|
201
|
+
width: 100,
|
|
202
|
+
dataIndex: 'size',
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
const handleTimeChange = (time: string) => {
|
|
206
|
+
onTimeChange && onTimeChange(time);
|
|
207
|
+
// 计算播放时间占起始时间当日的百分比
|
|
208
|
+
const progress =
|
|
209
|
+
(moment(time).diff(moment(state.channel?.beginTime).startOf('day'), 'seconds') /
|
|
210
|
+
(24 * 60 * 60)) *
|
|
211
|
+
100;
|
|
212
|
+
setState({ timeLinePosition: progress });
|
|
213
|
+
};
|
|
214
|
+
return (
|
|
215
|
+
<div className="vtx-history-control" style={{ width, height }}>
|
|
216
|
+
{state.name && <PlayerHeader name={state.name} onClose={handleClose} />}
|
|
217
|
+
<div className="player-container">
|
|
218
|
+
{state.playParams.url && (
|
|
219
|
+
<HistoryPlayer
|
|
220
|
+
{...state.playParams}
|
|
221
|
+
controlBottom={isExpanded ? 235 : 0}
|
|
222
|
+
onTimeChange={handleTimeChange}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
{state.tableData && state.tableData.length > 0 && (
|
|
227
|
+
<div className="collapse-controls">
|
|
228
|
+
<div className="action-bar" {...getToggleProps({})}>
|
|
229
|
+
<img
|
|
230
|
+
src={collapseBtnImg}
|
|
231
|
+
alt=""
|
|
232
|
+
style={{ transform: `rotate(${isExpanded ? 180 : 0}deg)` }}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
<section className="collapse-panel-container" {...getCollapseProps()}>
|
|
236
|
+
<div className="panels">
|
|
237
|
+
<div className="time-panel">
|
|
238
|
+
<div className="segments">{getSegments(state.segmentData)}</div>
|
|
239
|
+
{state.timeLinePosition >= 0 && (
|
|
240
|
+
<TimePositionIcon
|
|
241
|
+
className="time-line-icon"
|
|
242
|
+
style={{ left: `${state.timeLinePosition}%` }}
|
|
243
|
+
/>
|
|
244
|
+
)}
|
|
245
|
+
<TimeLine />
|
|
246
|
+
</div>
|
|
247
|
+
<div className="urls-panel">
|
|
248
|
+
<Table
|
|
249
|
+
className="history-url-table"
|
|
250
|
+
columns={columns}
|
|
251
|
+
dataSource={state.tableData}
|
|
252
|
+
scroll={{ y: 100 }}
|
|
253
|
+
pagination={false}
|
|
254
|
+
rowClassName={(_, index) =>
|
|
255
|
+
index === state.playParams.index ? 'active' : ''
|
|
256
|
+
}
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</section>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
<div className="bottom-controls">
|
|
264
|
+
<Space className="left" size={12} align="center">
|
|
265
|
+
{onExpandChange && (
|
|
266
|
+
<Tooltip title={state.expand ? '收起' : '展开'}>
|
|
267
|
+
<div onClick={handleExpand}>
|
|
268
|
+
{state.expand ? <FoldIcon /> : <ExpandIcon />}
|
|
269
|
+
</div>
|
|
270
|
+
</Tooltip>
|
|
271
|
+
)}
|
|
272
|
+
</Space>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
HistoryControl.displayName = 'HistoryControl';
|
|
279
|
+
export type UrlsTableItemType = {
|
|
280
|
+
/** 序号 */
|
|
281
|
+
index: number;
|
|
282
|
+
/** 设备名称 */
|
|
283
|
+
deviceName: string;
|
|
284
|
+
/** 通道名称 */
|
|
285
|
+
channelName: string;
|
|
286
|
+
/** 开始时间 */
|
|
287
|
+
beginTime: string;
|
|
288
|
+
/** 结束时间 */
|
|
289
|
+
endTime: string;
|
|
290
|
+
/** 视频时长 */
|
|
291
|
+
duration: string;
|
|
292
|
+
/** 视频大小 */
|
|
293
|
+
size: string;
|
|
294
|
+
/** 地址 */
|
|
295
|
+
url: string;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const TimeLine = () => {
|
|
299
|
+
const getSplitLines = () => {
|
|
300
|
+
const result = [];
|
|
301
|
+
const step = 100 / 24;
|
|
302
|
+
for (let i = 0; i < 25; i++) {
|
|
303
|
+
const element = (
|
|
304
|
+
<div key={i} className="line" style={{ left: `${step * i}%` }}>
|
|
305
|
+
<div className="number">{i >= 10 ? i : '0' + i}</div>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
result.push(element);
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
};
|
|
312
|
+
return (
|
|
313
|
+
<div className="time-axis">
|
|
314
|
+
<div className="axis" />
|
|
315
|
+
<div className="split-lines">{getSplitLines()}</div>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
type GetSegmentsProps = { list: HistoryUrlItem[]; start?: moment.Moment; end?: moment.Moment };
|
|
321
|
+
const getSegments = (props: GetSegmentsProps) => {
|
|
322
|
+
const { list, start, end } = props;
|
|
323
|
+
const startTime = moment(start);
|
|
324
|
+
const endTime = moment(end);
|
|
325
|
+
const total = endTime.diff(startTime, 'seconds'); // 总时间
|
|
326
|
+
const result = list.map((i) => {
|
|
327
|
+
const itemStart =
|
|
328
|
+
moment(i.beginTime).diff(startTime, 'seconds') > 0 ? moment(i.beginTime) : startTime;
|
|
329
|
+
const itemEnd = moment(i.endTime);
|
|
330
|
+
const itemTotal = itemEnd.diff(itemStart, 'seconds'); // 分段总时间
|
|
331
|
+
const width = (itemTotal / total) * 100; // 分段时间占总时间百分比
|
|
332
|
+
const left = (itemStart.diff(startTime, 'seconds') / total) * 100; // 分段其实时间
|
|
333
|
+
return (
|
|
334
|
+
<div
|
|
335
|
+
key={i.beginTime}
|
|
336
|
+
className="segment"
|
|
337
|
+
style={{ width: `${width}%`, left: `${left < 0 ? 0 : left}%` }}
|
|
338
|
+
/>
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
return result;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export default HistoryControl;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
.vtx-history-player {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
background-color: #2b3d51;
|
|
7
|
+
border: 1px solid transparent;
|
|
8
|
+
|
|
9
|
+
.controls {
|
|
10
|
+
position: absolute;
|
|
11
|
+
bottom: 0;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 30px;
|
|
16
|
+
padding: 0 12px;
|
|
17
|
+
background-color: #131d27;
|
|
18
|
+
border-bottom: 1px solid rgb(82 196 26 / 100%);
|
|
19
|
+
opacity: 0;
|
|
20
|
+
transition: opacity 0.5s;
|
|
21
|
+
|
|
22
|
+
.time {
|
|
23
|
+
padding: 4px 12px;
|
|
24
|
+
margin: 0 12px;
|
|
25
|
+
background-color: rgb(145 165 185 / 30%);
|
|
26
|
+
|
|
27
|
+
.current {
|
|
28
|
+
color: white;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.total {
|
|
32
|
+
color: rgb(255 255 255 / 85%);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.slider {
|
|
37
|
+
flex-grow: 1;
|
|
38
|
+
margin-right: 24px;
|
|
39
|
+
|
|
40
|
+
.ant-slider {
|
|
41
|
+
.ant-slider-rail {
|
|
42
|
+
background-color: rgb(173 176 180);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.right {
|
|
48
|
+
margin-left: auto;
|
|
49
|
+
|
|
50
|
+
> div {
|
|
51
|
+
height: 24px;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&:hover {
|
|
57
|
+
.controls {
|
|
58
|
+
opacity: 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
video {
|
|
63
|
+
width: 100%;
|
|
64
|
+
height: 100%;
|
|
65
|
+
object-fit: fill;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.sub-wnd {
|
|
69
|
+
width: 100% !important;
|
|
70
|
+
height: 100% !important;
|
|
71
|
+
pointer-events: none;
|
|
72
|
+
background-color: #2b3d51 !important;
|
|
73
|
+
border: 1px solid transparent !important;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.error-container {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: 50%;
|
|
79
|
+
left: 50%;
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
align-items: center;
|
|
83
|
+
transform: translate(-50%, -50%);
|
|
84
|
+
|
|
85
|
+
.text {
|
|
86
|
+
margin: 20px 0;
|
|
87
|
+
font-size: 14px;
|
|
88
|
+
color: rgb(255 255 255 / 65%);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.loading-container {
|
|
93
|
+
position: absolute;
|
|
94
|
+
top: 50%;
|
|
95
|
+
left: 50%;
|
|
96
|
+
transform: translate(-50%, -50%);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import HistoryPlayer from '.';
|
|
2
|
+
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
|
|
3
|
+
import { Alert, Space } from 'antd';
|
|
4
|
+
|
|
5
|
+
import * as stories from './index.stories';
|
|
6
|
+
|
|
7
|
+
<Meta title="组件/历史播放器" component={HistoryPlayer} />
|
|
8
|
+
|
|
9
|
+
# 历史播放器
|
|
10
|
+
|
|
11
|
+
## 引入
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { HistoryPlayer } from '@vtx/player';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 支持的格式
|
|
18
|
+
|
|
19
|
+
- 海康 ws,使用海康提供的 h5player.js 实现
|
|
20
|
+
- flv,使用 flv.js 实现
|
|
21
|
+
- hls,使用 hls.js 实现
|
|
22
|
+
|
|
23
|
+
## 注意
|
|
24
|
+
|
|
25
|
+
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
|
|
26
|
+
<Alert message="当type属性为空时,组件将自动根据url判断视频流格式" type="warning" />
|
|
27
|
+
<Alert message="flv格式视频流不支持进度条拖拽" type="warning" />
|
|
28
|
+
</Space>
|
|
29
|
+
|
|
30
|
+
## 属性
|
|
31
|
+
|
|
32
|
+
<ArgsTable of={HistoryPlayer} />
|
|
33
|
+
|
|
34
|
+
## 例子
|
|
35
|
+
|
|
36
|
+
<Canvas>
|
|
37
|
+
<Story name="历史播放器" story={stories.Default} />
|
|
38
|
+
</Canvas>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import HistoryPlayer from '.';
|
|
2
|
+
import { ComponentStory } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
const Template: ComponentStory<typeof HistoryPlayer> = (args) => <HistoryPlayer {...args} />;
|
|
5
|
+
|
|
6
|
+
export const Default = Template.bind({});
|
|
7
|
+
Default.args = {
|
|
8
|
+
url: '',
|
|
9
|
+
width: 1000,
|
|
10
|
+
height: 600,
|
|
11
|
+
};
|
|
12
|
+
export default Default;
|