pages_vod_upload.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import React, { useRef, useState } from "react";
  2. import { Button, Card, message, Progress, Space, Tag } from "antd";
  3. import TcVod from "vod-js-sdk-v6";
  4. import { VodAPI } from "./api/vod.ts";
  5. interface UploadTask {
  6. file: File;
  7. progress: number;
  8. status: "pending" | "uploading" | "success" | "error" | "canceled";
  9. fileId?: string;
  10. videoUrl?: string;
  11. coverUrl?: string;
  12. cancel?: () => void;
  13. }
  14. export const VodUploadPage = () => {
  15. const videoInputRef = useRef<HTMLInputElement>(null);
  16. const coverInputRef = useRef<HTMLInputElement>(null);
  17. const [uploadTasks, setUploadTasks] = useState<UploadTask[]>([]);
  18. const [selectedVideo, setSelectedVideo] = useState<File | null>(null);
  19. const [selectedCover, setSelectedCover] = useState<File | null>(null);
  20. // 获取上传签名
  21. const getSignature = async () => {
  22. try {
  23. return await VodAPI.getSignature();
  24. } catch (error) {
  25. message.error("获取上传签名失败");
  26. throw error;
  27. }
  28. };
  29. // 初始化VOD SDK
  30. const tcVod = new TcVod({
  31. getSignature: getSignature,
  32. });
  33. // 处理视频文件选择
  34. const handleVideoSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
  35. if (e.target.files && e.target.files[0]) {
  36. setSelectedVideo(e.target.files[0]);
  37. }
  38. };
  39. // 处理封面文件选择
  40. const handleCoverSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
  41. if (e.target.files && e.target.files[0]) {
  42. setSelectedCover(e.target.files[0]);
  43. }
  44. };
  45. // 开始上传
  46. const startUpload = async () => {
  47. if (!selectedVideo) {
  48. message.warning("请先选择视频文件");
  49. return;
  50. }
  51. const newTask: UploadTask = {
  52. file: selectedVideo,
  53. progress: 0,
  54. status: "pending",
  55. };
  56. setUploadTasks((prev) => [...prev, newTask]);
  57. try {
  58. const uploader = tcVod.upload({
  59. mediaFile: selectedVideo,
  60. coverFile: selectedCover || undefined,
  61. });
  62. const updatedTask: UploadTask = {
  63. ...newTask,
  64. status: "uploading",
  65. cancel: () => {
  66. uploader.cancel();
  67. setUploadTasks((prev) =>
  68. prev.map((task) =>
  69. task.file === newTask.file
  70. ? { ...task, status: "canceled" }
  71. : task
  72. )
  73. );
  74. },
  75. };
  76. setUploadTasks((prev) =>
  77. prev.map((task) => task.file === newTask.file ? updatedTask : task)
  78. );
  79. // 监听上传进度
  80. uploader.on("media_progress", (info) => {
  81. setUploadTasks((prev) =>
  82. prev.map((task) =>
  83. task.file === newTask.file
  84. ? { ...task, progress: info.percent * 100 }
  85. : task
  86. )
  87. );
  88. });
  89. // 监听上传完成
  90. uploader.on("media_upload", (info) => {
  91. setUploadTasks((prev) =>
  92. prev.map((task) =>
  93. task.file === newTask.file ? { ...task, status: "success" } : task
  94. )
  95. );
  96. });
  97. // 执行上传
  98. const result = await uploader.done();
  99. setUploadTasks((prev) =>
  100. prev.map((task) =>
  101. task.file === newTask.file
  102. ? {
  103. ...task,
  104. fileId: result.fileId,
  105. videoUrl: result.video.url,
  106. coverUrl: result.cover?.url,
  107. status: "success",
  108. }
  109. : task
  110. )
  111. );
  112. message.success("视频上传成功");
  113. } catch (error) {
  114. setUploadTasks((prev) =>
  115. prev.map((task) =>
  116. task.file === newTask.file ? { ...task, status: "error" } : task
  117. )
  118. );
  119. message.error("视频上传失败");
  120. } finally {
  121. setSelectedVideo(null);
  122. setSelectedCover(null);
  123. if (videoInputRef.current) videoInputRef.current.value = "";
  124. if (coverInputRef.current) coverInputRef.current.value = "";
  125. }
  126. };
  127. // 渲染上传状态标签
  128. const renderStatusTag = (status: UploadTask["status"]) => {
  129. switch (status) {
  130. case "pending":
  131. return <Tag color="default">等待上传</Tag>;
  132. case "uploading":
  133. return <Tag color="processing">上传中</Tag>;
  134. case "success":
  135. return <Tag color="success">上传成功</Tag>;
  136. case "error":
  137. return <Tag color="error">上传失败</Tag>;
  138. case "canceled":
  139. return <Tag color="warning">已取消</Tag>;
  140. default:
  141. return <Tag color="default">未知状态</Tag>;
  142. }
  143. };
  144. return (
  145. <div>
  146. <Card title="视频上传" className="mb-4">
  147. <Space direction="vertical" style={{ width: "100%" }}>
  148. <div>
  149. <input
  150. type="file"
  151. ref={videoInputRef}
  152. onChange={handleVideoSelect}
  153. accept="video/*"
  154. style={{ display: "none" }}
  155. />
  156. <Button
  157. onClick={() => videoInputRef.current?.click()}
  158. type="primary"
  159. >
  160. {selectedVideo ? selectedVideo.name : "选择视频文件"}
  161. </Button>
  162. <input
  163. type="file"
  164. ref={coverInputRef}
  165. onChange={handleCoverSelect}
  166. accept="image/*"
  167. style={{ display: "none", marginLeft: 16 }}
  168. />
  169. <Button
  170. onClick={() => coverInputRef.current?.click()}
  171. style={{ marginLeft: 16 }}
  172. >
  173. {selectedCover ? selectedCover.name : "选择封面(可选)"}
  174. </Button>
  175. <Button
  176. type="primary"
  177. onClick={startUpload}
  178. disabled={!selectedVideo}
  179. style={{ marginLeft: 16 }}
  180. >
  181. 开始上传
  182. </Button>
  183. </div>
  184. <div style={{ marginTop: 24 }}>
  185. {uploadTasks.map((task, index) => (
  186. <div key={index} style={{ marginBottom: 16 }}>
  187. <div style={{ marginBottom: 8 }}>
  188. <span style={{ marginRight: 8 }}>{task.file.name}</span>
  189. {renderStatusTag(task.status)}
  190. {task.status === "uploading" && task.cancel && (
  191. <Button
  192. type="link"
  193. danger
  194. onClick={task.cancel}
  195. style={{ marginLeft: 8 }}
  196. >
  197. 取消上传
  198. </Button>
  199. )}
  200. </div>
  201. {task.status === "uploading" && (
  202. <Progress percent={task.progress} status="active" />
  203. )}
  204. {task.fileId && (
  205. <div style={{ marginTop: 8 }}>
  206. <div>File ID: {task.fileId}</div>
  207. {task.videoUrl && (
  208. <div>
  209. 视频地址:{" "}
  210. <a
  211. href={task.videoUrl}
  212. target="_blank"
  213. rel="noreferrer"
  214. >
  215. {task.videoUrl}
  216. </a>
  217. </div>
  218. )}
  219. {task.coverUrl && (
  220. <div>
  221. 封面地址:{" "}
  222. <a
  223. href={task.coverUrl}
  224. target="_blank"
  225. rel="noreferrer"
  226. >
  227. {task.coverUrl}
  228. </a>
  229. </div>
  230. )}
  231. </div>
  232. )}
  233. </div>
  234. ))}
  235. </div>
  236. </Space>
  237. </Card>
  238. </div>
  239. );
  240. };