publish-video.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. <template>
  2. <v-row justify="center" align="center">
  3. <v-col>
  4. <v-card
  5. class="mx-auto"
  6. outlined
  7. >
  8. <v-row justify="center">
  9. <v-col>
  10. <uploader
  11. class="uploader-example"
  12. :options="options"
  13. :auto-start="true"
  14. @file-added="onFileAdded"
  15. @file-success="onFileSuccess"
  16. @file-progress="onFileProgress"
  17. @file-error="onFileError"
  18. >
  19. <uploader-unsupport />
  20. <uploader-drop>
  21. <p>拖动视频文件到此处或</p>
  22. <uploader-btn :attrs="attrs">选择视频文件</uploader-btn>
  23. </uploader-drop>
  24. <uploader-list />
  25. </uploader>
  26. </v-col>
  27. </v-row>
  28. <v-row justify="center">
  29. <v-col cols="5">
  30. <v-card outlined>
  31. <v-img :src="coverUrl" aspect-ratio="1.77" contain max-height="150" alt="封面图,推荐16:9" />
  32. </v-card>
  33. </v-col>
  34. <!-- <v-col cols="5">
  35. <v-file-input
  36. :rules="rules"
  37. accept="image/png, image/jpeg, image/bmp"
  38. prepend-icon="mdi-image"
  39. placeholder="上传视频封面"
  40. label="封面"
  41. @change="setFile"
  42. />
  43. <v-btn color="primary" @click="uploadVideoCover">
  44. 上传
  45. </v-btn>
  46. </v-col>-->
  47. </v-row>
  48. <v-divider />
  49. <v-row justify="center">
  50. <v-col cols="10">
  51. <h2>稿件信息</h2>
  52. </v-col>
  53. </v-row>
  54. <v-row justify="center">
  55. <v-col cols="5">
  56. <v-select
  57. :items="category"
  58. label="分区"
  59. @change="getCategory"
  60. />
  61. </v-col>
  62. <v-col cols="5">
  63. <v-select
  64. :items="childCategory"
  65. label="子分区"
  66. @change="getChildCategory"
  67. />
  68. </v-col>
  69. </v-row>
  70. <v-row justify="center">
  71. <v-col cols="10">
  72. <v-text-field
  73. v-model="videoPost.title"
  74. placeholder="标题"
  75. label="标题(50字以内)"
  76. clearable
  77. />
  78. </v-col>
  79. </v-row>
  80. <v-row justify="center">
  81. <v-col cols="10">
  82. <v-textarea
  83. v-model="videoPost.description"
  84. label="简介(200字以内)"
  85. clearable
  86. placeholder="填写更全面的视频信息,让更多的人找到你!"
  87. />
  88. </v-col>
  89. </v-row>
  90. <v-row justify="center">
  91. <v-col cols="10">
  92. <v-combobox
  93. v-model="videoPost.tags"
  94. label="添加标签让更多人找到你(最多6个)"
  95. multiple
  96. chips
  97. clearable
  98. />
  99. </v-col>
  100. </v-row>
  101. <!-- <v-row justify="center">
  102. <v-col cols="10">
  103. <v-select
  104. :items="scope"
  105. label="可见范围"
  106. @change="setVideoScope"
  107. />
  108. </v-col>
  109. </v-row>-->
  110. <v-row justify="center">
  111. <v-col cols="10">
  112. <v-btn large color="primary" @click="publish">立即投稿</v-btn>
  113. </v-col>
  114. </v-row>
  115. </v-card>
  116. </v-col>
  117. <v-snackbar
  118. v-model="showMessage"
  119. :top="true"
  120. :timeout="3000"
  121. >
  122. {{ message }}
  123. <template v-slot:action="{ attrs }">
  124. <v-btn
  125. color="pink"
  126. text
  127. v-bind="attrs"
  128. @click="showMessage = false"
  129. >
  130. 关闭
  131. </v-btn>
  132. </template>
  133. </v-snackbar>
  134. </v-row>
  135. </template>
  136. <script>
  137. import { videoCategory, submitVideoPost } from '@/api/media/video'
  138. import { getVideoId } from '@/api/media/file'
  139. /* import { hashFile } from '@/utils/hash' */
  140. export default {
  141. data() {
  142. return {
  143. options: {
  144. target: '//oss.reghao.cn',
  145. chunkSize: 1024 * 1024 * 1024 * 5, // 5GiB
  146. fileParameterName: 'file',
  147. testChunks: false,
  148. query: (file, chunk) => {
  149. const key = 'video/playback/' + this.videoPost.videoUrlId
  150. return { key: key }
  151. },
  152. headers: {
  153. }
  154. },
  155. attrs: {
  156. accept: 'video/*'
  157. },
  158. rules: [
  159. value => !value || value.size < 2000000 || 'Avatar size should be less than 2 MB!'
  160. ],
  161. coverUrl: null,
  162. // 提交给后端的数据
  163. videoPost: {
  164. videoFileId: null,
  165. videoUrlId: null,
  166. coverFileId: null,
  167. title: null,
  168. description: null,
  169. categoryId: null,
  170. tags: [],
  171. scope: 1,
  172. width: null,
  173. height: null,
  174. duration: null
  175. },
  176. categoryMap: {
  177. Set: function(key, value) { this[key] = value },
  178. Get: function(key) { return this[key] },
  179. Contains: function(key) { return this.Get(key) !== null },
  180. Remove: function(key) { delete this[key] }
  181. },
  182. category: [],
  183. childCategory: [],
  184. scope: [
  185. '所有人可见',
  186. '验证码可见',
  187. 'VIP 可见',
  188. '仅自己可见'
  189. ],
  190. nowCategory: {},
  191. coverFile: null,
  192. showMessage: false,
  193. message: ''
  194. }
  195. },
  196. created() {
  197. this.getVideoIdWrapper()
  198. this.getVideoCategory()
  199. },
  200. methods: {
  201. // 选择视频后获取视频的分辨率和时长, 并截取第一秒的内容作为封面
  202. processVideo(file) {
  203. return new Promise((resolve, reject) => {
  204. const canvas = document.createElement('canvas')
  205. const canvasCtx = canvas.getContext('2d')
  206. const videoElem = document.createElement('video')
  207. const dataUrl = window.URL.createObjectURL(file)
  208. // 当前帧的数据是可用的
  209. videoElem.onloadeddata = function() {
  210. resolve(videoElem)
  211. }
  212. videoElem.onerror = function() {
  213. reject('video 后台加载失败')
  214. }
  215. // 设置 auto 预加载数据, 否则会出现截图为黑色图片的情况
  216. videoElem.setAttribute('preload', 'auto')
  217. videoElem.src = dataUrl
  218. // 预加载完成后才会获取到视频的宽高和时长数据
  219. videoElem.addEventListener('canplay', this.onCanPlay(videoElem, canvas, canvasCtx))
  220. })
  221. },
  222. onCanPlay(videoElem, canvas, canvasCtx) {
  223. setTimeout(() => {
  224. // 视频视频分辨率
  225. const videoWidth = videoElem.videoWidth
  226. const videoHeight = videoElem.videoHeight
  227. this.videoPost.width = videoWidth
  228. this.videoPost.height = videoHeight
  229. this.videoPost.duration = videoElem.duration
  230. videoElem.pause()
  231. /* const ratio = window.devicePixelRatio || 1
  232. canvasCtx.scale(ratio, ratio)*/
  233. // 设置画布尺寸
  234. canvas.width = videoWidth
  235. canvas.height = videoHeight
  236. canvasCtx.drawImage(videoElem, 0, 0, canvas.width, canvas.height)
  237. // 把图标base64编码后变成一段url字符串
  238. const urlData = canvas.toDataURL('image/jpeg')
  239. if (typeof urlData !== 'string') {
  240. alert('urlData不是字符串')
  241. return
  242. }
  243. var arr = urlData.split(',')
  244. var bstr = atob(arr[1])
  245. var n = bstr.length
  246. var u8arr = new Uint8Array(n)
  247. while (n--) {
  248. u8arr[n] = bstr.charCodeAt(n)
  249. }
  250. const coverFile = new File([u8arr], 'cover.jpg', { type: 'image/jpeg' })
  251. if (coverFile instanceof File) {
  252. const formData = new FormData()
  253. formData.append('file', coverFile)
  254. fetch(`//api.reghao.cn/api/file/upload/image`, {
  255. headers: {},
  256. method: 'POST',
  257. credentials: 'include',
  258. body: formData
  259. }).then(response => response.json())
  260. .then(json => {
  261. if (json.code === 0) {
  262. this.videoPost.coverFileId = json.data.imageFileId
  263. this.coverUrl = json.data.imageUrl
  264. } else {
  265. this.message = '视频封面上传失败,请重试!' + json.message
  266. this.showMessage = true
  267. }
  268. })
  269. .catch(e => {
  270. return null
  271. })
  272. }
  273. }, 1000) // 1000毫秒,就是截取第一秒,2000毫秒就是截取第2秒,视频1秒通常24帧,也可以换算成截取第几帧。
  274. // 防止拖动进度条的时候重复触发
  275. // videoElem.removeEventListener('canplay', arguments.callee)
  276. },
  277. onFileAdded(file) {
  278. this.setTitle(file.file.name)
  279. this.processVideo(file.file)
  280. /* file.pause()
  281. hashFile(file.file).then(res => {
  282. const formData = new FormData()
  283. formData.append('filename', file.file.name)
  284. formData.append('size', file.file.size)
  285. formData.append('sha256sum', res.sha256sum)
  286. fetch(`//file.reghao.cn` + `/api/file/upload/video/prepare`, {
  287. headers: {
  288. Authorization: 'Bearer ' + this.$store.getters.token
  289. },
  290. method: 'POST',
  291. credentials: 'include',
  292. body: formData
  293. }).then(response => response.json())
  294. .then(json => {
  295. const uploadId = json.data.uploadId
  296. const exist = json.data.exist
  297. if (exist) {
  298. this.message = '视频已存在'
  299. this.showMessage = true
  300. file.cancel()
  301. } else {
  302. file.uniqueIdentifier = uploadId
  303. file.resume()
  304. }
  305. })
  306. .catch(e => {
  307. return null
  308. })
  309. })*/
  310. },
  311. onFileProgress(rootFile, file, chunk) {
  312. },
  313. onFileSuccess(rootFile, file, response, chunk) {
  314. const res = JSON.parse(response)
  315. if (res.code === 0) {
  316. this.message = '视频已上传'
  317. this.showMessage = true
  318. }
  319. },
  320. onFileError(rootFile, file, response, chunk) {
  321. console.log('文件上传错误')
  322. },
  323. publish() {
  324. if (!this.videoPost.videoFileId) {
  325. this.message = '你还没有上传视频'
  326. this.showMessage = true
  327. return
  328. }
  329. if (!this.videoPost.coverFileId) {
  330. this.message = '你还没有上传视频封面'
  331. this.showMessage = true
  332. return
  333. }
  334. if (this.videoPost.title === '' || this.videoPost.categoryId === -1) {
  335. this.message = '分区和稿件标题不能为空'
  336. this.showMessage = true
  337. return
  338. }
  339. /* if (this.videoPost.scope === null) {
  340. this.message = '稿件可见范围不能为空'
  341. this.showMessage = true
  342. return
  343. }*/
  344. if (this.videoPost.tags.length === 0 || this.videoPost.tags.length > 10) {
  345. this.message = '标签最少 1 个, 最多 10 个'
  346. this.showMessage = true
  347. return
  348. }
  349. submitVideoPost(this.videoPost)
  350. .then(res => {
  351. if (res.code === 0) {
  352. this.message = '投稿成功,等待审核通过后其他人就可以看到你的视频了'
  353. this.showMessage = true
  354. this.$router.push('/studio')
  355. } else {
  356. this.message = res.msg
  357. this.showMessage = true
  358. }
  359. })
  360. .catch(error => {
  361. console.error(error.message)
  362. })
  363. },
  364. setFile(value) {
  365. this.coverFile = value
  366. },
  367. setTitle(title) {
  368. if (title.length > 50) {
  369. this.videoPost.title = title.substring(0, 50)
  370. this.videoPost.description = title
  371. } else {
  372. this.videoPost.title = title
  373. }
  374. },
  375. /* uploadVideoCover() {
  376. if (this.coverFile === null) {
  377. this.message = '请先选择视频封面,然后上传!'
  378. this.showMessage = true
  379. return
  380. }
  381. if (this.videoPost.videoFileId === null) {
  382. this.message = '等待视频上传完成后再上传封面!'
  383. this.showMessage = true
  384. return
  385. }
  386. const formData = new FormData()
  387. formData.append('videoFileId', this.videoPost.videoFileId)
  388. formData.append('file', this.coverFile)
  389. fetch(`//api.reghao.cn/api/file/upload/video/cover`, {
  390. headers: {
  391. 'Authorization': 'Bearer ' + this.$store.getters.token
  392. },
  393. method: 'POST',
  394. credentials: 'include',
  395. body: formData
  396. }).then(response => response.json())
  397. .then(json => {
  398. if (json.code === 0) {
  399. this.message = '封面已上传'
  400. this.showMessage = true
  401. this.videoPost.coverFileId = json.data.imageFileId
  402. this.videoPost.imageUrl = json.data.imageUrl
  403. } else {
  404. this.message = '上传失败,请重试!' + json.message
  405. this.showMessage = true
  406. }
  407. })
  408. .catch(e => {
  409. return null
  410. })
  411. },*/
  412. getVideoCategory() {
  413. videoCategory()
  414. .then(res => {
  415. if (res.code === 0) {
  416. for (let i = 0; i < res.data.length; i++) {
  417. const name = res.data[i].name
  418. this.category.push(name)
  419. this.categoryMap.Set(name, res.data[i])
  420. }
  421. } else {
  422. console.error(res.msg)
  423. }
  424. })
  425. .catch(error => {
  426. console.error(error.message)
  427. })
  428. },
  429. getCategory(name) {
  430. // 重置子分区,清除前一次选择分区时留下的缓存
  431. this.childCategory = []
  432. this.currentCategory = this.categoryMap.Get(name)
  433. this.videoPost.categoryId = this.currentCategory.id
  434. const c = this.currentCategory.children
  435. if (c) {
  436. for (let i = 0; i < c.length; i++) {
  437. this.childCategory.push(c[i].name)
  438. }
  439. }
  440. },
  441. getChildCategory(name) {
  442. const c = this.currentCategory.children
  443. for (let i = 0; i < c.length; i++) {
  444. if (c[i].name === name) {
  445. this.videoPost.categoryId = c[i].id
  446. }
  447. }
  448. },
  449. setVideoScope(scope) {
  450. if (scope === '所有人可见') {
  451. this.videoPost.scope = 1
  452. } else if (scope === '验证码可见') {
  453. this.videoPost.scope = 2
  454. } else if (scope === 'VIP 可见') {
  455. this.videoPost.scope = 3
  456. } else if (scope === '仅自己可见') {
  457. this.videoPost.scope = 4
  458. }
  459. },
  460. getVideoIdWrapper() {
  461. getVideoId(this.videoPost)
  462. .then(res => {
  463. if (res.code === 0) {
  464. console.log(res.data)
  465. this.videoPost.videoFileId = res.data.videoFileId
  466. this.videoPost.videoUrlId = res.data.videoUrlId
  467. } else {
  468. this.message = res.msg
  469. this.showMessage = true
  470. }
  471. })
  472. .catch(error => {
  473. console.error(error.message)
  474. })
  475. }
  476. }
  477. }
  478. </script>
  479. <style>
  480. .uploader-example {
  481. width: 500px;
  482. padding: 15px;
  483. margin: 40px auto 0;
  484. font-size: 12px;
  485. box-shadow: 0 0 10px rgba(0, 0, 0, .4);
  486. }
  487. .uploader-example .uploader-btn {
  488. margin-right: 4px;
  489. }
  490. .uploader-example .uploader-list {
  491. max-height: 440px;
  492. overflow: auto;
  493. overflow-x: hidden;
  494. overflow-y: auto;
  495. }
  496. </style>