一、<video>標簽解析
<video
ref="video"
x5-video-player-type="h5"
x5-video-player-fullscreen="true"
x5-playsinline
webkit-playsinline
playsinline
preload="auto"
controlslist="nodownload noremoteplayback"
disablePictureInPicture
:src="src"
:poster="videoImage"
@play="onVideoPlay"
@pause="onVideoPause"
@ended="onEnded"
@timeupdate="onVideoTimeUpdate"
@error="onVideoError"
@waiting="onWaiting"
@click="changePlay"
@loadedmetadata="loadedmetadata"
@loadeddata="loadeddata"
@canplay="onVideoCanPlay"
@canplaythrough="canplaythrough"
@seeking="onSeeking"
@seeked="onseeked"
/>
常用屬性解析
x5-video-player-type:(只支持安卓)啟用同層H5播放器,就是在視頻全屏的時候,div可以呈現在視頻層上,也是WeChat安卓版特有的屬性。同層播放別名也叫做沉浸式播放,播放的時候看似全屏,但是已經除去了control和微信的導航欄,只留下"X"和"<"兩鍵。目前的同層播放器只在Android(包括微信)上生效,暫時不支持iOS。至于為什么同層播放只對安卓開放,是因為安卓不能像IOS一樣局域播放,默認的全屏會使得一些界面操作被阻攔,如果是全屏H5還好,但是做直播的話,諸如彈幕那樣的功能就無法實現了,所以這時候同層播放的概念就解決了這個問題。
x5-video-player-fullscreen:(只支持安卓)全屏設置。它又兩個屬性值,ture和false,true支持全屏播放,false不支持全屏播放。IOS 微信瀏覽器是Chrome的內核,不支持X5同層播放。安卓微信瀏覽器是X5內核,一些屬性標簽比如playsinline就不支持,所以始終全屏。
x5-playsinline:(只支持安卓)使視頻內聯播放
webkit-playsinline:可以使視頻內聯播放(解決IOS端,Android不支持)
playsinline:IOS微信瀏覽器支持小窗內播放,布爾屬性,指明視頻將內聯(inline)播放,即在元素的播放區域內。請注意,沒有此屬性并不意味著視頻始終是全屏播放的。
preload就:該枚舉屬性旨在提示瀏覽器,作者認為在播放視頻之前,加載哪些內容會達到最佳的用戶體驗。可能是下列值之一:
none: 表示不應該預加載視頻。
metadata: 表示僅預先獲取視頻的元數據(例如長度)。
auto: 表示可以下載整個視頻文件,即使用戶不希望使用它。
空字符串: 和值為 auto 一致。每個瀏覽器的默認值都不相同,即使規范建議設置為 metadata。
controls:加上這個屬性,瀏覽器會在視頻底部提供一個控制面板,允許用戶控制視頻的播放,包括音量,跨幀,暫停/恢復播放。
controlslist :當瀏覽器顯示視頻底部的播放控制面板(例如,指定了 controls 屬性)時,controlslist 屬性會幫助瀏覽器選擇在控制面板上顯示哪些控件。允許接受的值有 nodownload, nofullscreen 和 noremoteplayback。如果要禁用畫中畫模式(和控件),請使用 disablePictureInPicture 屬性。
disablePictureInPicture:防止瀏覽器顯示畫中畫上下文菜單或在某些情況下自動請求畫中畫模式。該屬性可以禁用 video 元素的畫中畫特性,右鍵菜單中的“畫中畫”選項會被禁用。
src:要嵌到頁面的視頻的 URL。可選;你也可以使用 video 塊內的 <source> 元素來指定需要嵌到頁面的視頻。
poster:海報幀圖片 URL,用于在視頻處于下載中的狀態時顯示。如果未指定該屬性,則在視頻第一幀可用之前不會顯示任何內容,然后將視頻的第一幀會作為海報(poster)幀來顯示。
loop:布爾屬性;指定后,會在視頻播放結束的時候,自動返回視頻開始的地方,繼續播放。
muted:布爾屬性,指明在視頻中音頻的默認設置。設置后,音頻會初始化為靜音。默認值是 false, 意味著視頻播放的時候音頻也會播放。
常用事件解析
play:播放已開始。
pause:播放已暫停。
ended:視頻停止播放,因為 media 已經到達結束點。(視頻已播完)
timeupdate:currentTime 屬性指定的時間發生變化。(視頻播放時間變化,可以獲取event.target.currentTime, event.target.duration)
error:視頻錯誤處理
waiting:由于暫時缺少數據,播放已停止。(視頻流加載中或者視頻播放暫停)
click:點擊視頻
loadedmetadata:已加載元數據。
loadeddata:media 中的首幀已經完成加載。
canplay:瀏覽器可以播放媒體文件了,但估計沒有足夠的數據來支撐播放到結束,不必停下來進一步緩沖內容。(視頻可以開始,但不確定是否會播放)
canplaythrough:瀏覽器估計它可以在不停止內容緩沖的情況下播放媒體直到結束。(視頻一定可以可播放)
seeking:跳幀(seek)操作開始。
seeked:跳幀(seek)操作完成。
視頻內聯播放處理
為兼容ios和安卓,需要加上這幾個屬性:
x5-video-player-type="h5"
x5-video-player-fullscreen="true"
x5-playsinline
webkit-playsinline
playsinline
自動播放
android始終不能自動播放。ios10后版本的safari和微信都不讓視頻自動播放了(順帶音頻也不能自動播放了),但微信提供了一個事件WeixinJSBridgeReady,在微信嵌入webview全局的這個事件觸發后,視頻仍可以自動播放,這個應該是現在在ios端微信的視頻自動播放的比較靠譜的方式,其他如手q或者其他瀏覽器,建議就引導用戶觸發觸屏的行為后操作比較好。
// 引用微信jssdk
<script-- src="http://xxxx/jweixin-1.6.0.js"></script>
// 頁面初始化后執行自動播放
wx && wx.getNetworkType({
complete: () => {
const videoEle = this.$refs.video
videoEle.play()
}
})
// 或者這樣
WeixinJSBridge.invoke('getNetworkType', {}, (e)=> {
const videoEle = this.$refs.video
videoEle.play()
});
二、調起系統攝像獲取視頻及視頻時長
本地上傳支持格式
視頻在ios上本地上傳后能播放的文件類型:mov,mp4,m4v(微信會壓縮視頻,而且限制大小,mov或者mp4后綴改m4v可防止視頻在微信中被壓縮),3gp(手機上能播放,但是手機上input標簽選不了3gp格式的視頻)
視頻在安卓上本地上傳后能播放的文件類型:mov,mp4,m4v,3gp(手機上能播放,但是手機上input標簽選不了3gp格式的視頻),webm
總結:視頻在ios,安卓能在本地上傳后都能播放的文件類型:mov,mp4,m4v
手機錄像支持格式
視頻在錄像中支持上傳的文件類型(手機自帶攝像基本覆蓋ios和安卓了):mov,mp4
三、h5調起系統攝像功能&獲取視頻時長
// html
<input ref="input" accept="video/*" type="file" capture="user" @change="onChange" />
<!-- 虛擬組件,不做展示,用來獲取視頻時長 -->
<video
ref="video"
:src="videoUrl"
class="hidden-video"
:muted="false"
controls="controls"
@loadedmetadata="loadedmetadata"
></video>
// js
/** 錄制 */
onChange(event) {
const { target } = event
const file = target.files[0]
this.videoFile = file
if (!file) return
this.isLoadedmetadata = false
this.isSubmit = false
// 釋放內存
if (this.videoUrl) {
URL.revokeObjectURL(this.videoUrl)
}
// 這句后會執行loadedmetadata方法往下走
this.videoUrl = URL.createObjectURL(file)
console.log('videoUrl: ', this.videoUrl)
// 需要先加載微信jssdk
// 自動播放hack:兼容ios小程序webview中用戶手動點擊播放才會觸發加載事件
try {
if (window.wx) {
wx.getNetworkType({
complete: () => {
const videoEle = this.$refs.video
videoEle.play()
videoEle.pause()
}
})
}
} catch (e) {
console.log(e)
}
setTimeout(() => {
// 兜底:如果不兼容loadedmetadata則直接提交視頻
if (!this.isLoadedmetadata) {
this.isSubmit = true
this._videoUpload()
}
}, 2000)
},
/** 已加載好元數據:微信及瀏覽器支持 */
loadedmetadata(event) {
console.log('loadedmetadata 視頻時長為', event.target.duration)
if (this.isSubmit) return
this.isLoadedmetadata = true
this.videoDuration = event.target.duration || 0
this._videoUpload()
},
_videoUpload() {}
調起視頻錄制
:當accept="video/*"時capture只有兩種值,一種是user,用于調用面向人臉的攝像頭(例如手機前置攝像頭),一種是environment,用于調用環境攝像頭(例如手機后置攝像頭)
獲取視頻時長
:通過input的file拿到video鏈接URL.createObjectURL(file),觸發loadedmetadata事件拿到視頻時長,需要注意的是ios的小程序webview中需要觸發自動播放后才能觸發加載事件。
資源預覽
:通過input=flie得到文件file對象,如果需要預覽,不管是視頻還是圖片,可以通過URL.createObjectURL()來實現格式轉換。通過此方法會得到本地的blob URL,然后將此URL賦值給img的src,或者video的src,即可預覽。
export const getFileSrc = (file) => {
return URL.createObjectURL(file)
}
用戶錄像后video加載事件兼容
環境 | 安卓 | ios |
---|---|---|
瀏覽器 | loadedmetadata,loadeddata,canplaythrough支持 | 只loadedmetadata支持,自動播放后三個都支持 |
微信 | loadedmetadata,loadeddata,canplaythrough支持 | 只loadedmetadata支持,自動播放后三個都支持 |
小程序內嵌h5 | loadedmetadata,loadeddata,canplaythrough | 支持 都不支持,自動播放后三個都支持 |
四、視頻及其他文件通用校驗
通過對文件類型及大小進行校驗判斷。
// 文件類型
export const FILE_TYPE = {
VIDEO: 'video',
IMAGE: 'image',
AUDIO: 'audio'
}
// 錯誤類型
export const ERROR_TYPE = {
ERROR_FILE_TYPE: 'ERROR_FILE_TYPE',
ERROR_FILE_SIZE: 'ERROR_FILE_SIZE'
}
// 文案
const TYPE_MAP_TEXT = {
[FILE_TYPE.VIDEO]: '視頻',
[FILE_TYPE.IMAGE]: '圖片',
[FILE_TYPE.AUDIO]: '音頻'
}
// 限制類型
const ACCEPT_TYPE = {
[FILE_TYPE.VIDEO]: ['mp4', 'mov', 'm4v'],
[FILE_TYPE.IMAGE]: ['png', 'jpeg', 'jpg'],
[FILE_TYPE.AUDIO]: ['mp3', 'wav', 'mov']
}
// 限制大小: 單位M
const MAX_SIZE = {
[FILE_TYPE.VIDEO]: 50,
[FILE_TYPE.IMAGE]: 10,
[FILE_TYPE.AUDIO]: 30
}
/**
* 上傳文件校驗
* @param {File} file 文件<arrayBuffer>
* @param {Number} type 需要判斷的文件類型:視頻video,圖片image,音頻audio
* @param {Array} acceptType 限制的文件類型
* @param {Array} maxSize 限制的文件大小,單位 M
* @param {String} tipMsg 錯誤提示
* @returns { errorMsg: 錯誤信息, errorType: 錯誤類型 }
*/
export default function ({
file,
type = FILE_TYPE.VIDEO,
acceptType,
maxSize,
tipMsg = '上傳',
errorFileTypeMsg = '',
errorFileSizeMsg = ''
} = {}) {
let errorMsg = ''
let errorType = ''
if (!file) return
const fileMimeType = file.type
const fileName = file.name
const fileType = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
const fileSize = file.size / 1024 / 1024 // M
const fileAcceptType = acceptType || ACCEPT_TYPE[type]
const fileMaxSize = maxSize || MAX_SIZE[type]
const pageId = log.getPageId()
console.log('file:', file)
console.log(`${pageId}_upload_${type}_info`, `${fileType}_${fileSize}`)
const isValidMimeType = fileMimeType.includes(type)
// 判斷類型
if (!isValidMimeType || (isValidMimeType && !fileAcceptType.includes(fileType))) {
console.log(`${pageId}_upload_type_error`, fileType)
errorMsg =
errorFileTypeMsg || `不支持當前${TYPE_MAP_TEXT[type]}格式,僅支持${fileAcceptType.join('、')} ,請重新${tipMsg}`
errorType = ERROR_TYPE.ERROR_FILE_TYPE
}
// 判斷大小
else if (fileSize > fileMaxSize) {
console.log(`${pageId}_upload_size_over`, fileSize)
errorMsg = errorFileSizeMsg || `${TYPE_MAP_TEXT[type]}大小不能超過${fileMaxSize}M,請重新${tipMsg}`
errorType = ERROR_TYPE.ERROR_FILE_SIZE
}
return { errorMsg, errorType }
}
調用通用校驗:
const validInfo = isValidFile({
file: this.videoFile,
type: FILE_TYPE.VIDEO,
acceptType: ['mp4', 'mov'],
maxSize: 30,
tipMsg: '錄制',
errorFileSizeMsg: `視頻過大,請在3-5秒內錄制`
})
if (validInfo.errorMsg) {
return this.$toast.show({ content: validInfo.errorMsg })
}
五、視頻分片上傳
視頻分片思路:
1、將文件按照指定分片分成n等分,最后一份為剩余份數
2、按照視頻分片順序依次上傳文件分片給后端
3、按照當前視頻次序計算上傳進度
4、上傳成功/失敗后回調
5、后端拿到分片視頻按照視頻順序進行拼接,進行存儲或者返回給前端
import { request } from '@/plugins/request'
import { UPLOAD_CHUNK_FILE } from '@const/api/modules/common'
const SHARD_UPLOAD_SIZE = 5 // 分片限制5M
/**
* 分片上傳
* @param {File} file 文件<arrayBuffer>
* @param {Number} maxChunkSize 分片最大size,默認5M
* @param {Object} params 額外的業務參數
* @param {Function} onProgress 上傳進度回調
* @param {Function} onSuccess 分片上傳完畢回調
* @param {Function} onError 錯誤回調
* @returns 文件md5
*/
export class ChunkUpload {
constructor({
api = UPLOAD_CHUNK_FILE,
file,
maxChunkSize = SHARD_UPLOAD_SIZE,
params = {},
onProgress = () => {},
onSuccess = () => {},
onError = () => {}
} = {}) {
this.api = api
this.file = file // 源文件
this.fileSize = file.size // 文件大小
this.fileName = file.name // 文件名
this.shardSize = maxChunkSize * 1024 * 1024 // 分片大小
this.shardCount = Math.ceil(this.fileSize / this.shardSize) // 分片數
this.index = 0 // 當前分片序號 從0開始
this.params = params
this.fileInfoId = ''
this.isUploading = false
this.progress = 0 // 整個文件上傳進度
this.onProgress = onProgress
this.onSuccess = onSuccess
this.onError = onError
}
async upload() {
// id:時間_文件大小
this.fileInfoId = `${new Date().getTime()}_${this.fileSize}`
this.uploadHandle()
}
async uploadHandle() {
this.isUploading = true
const start = this.index * this.shardSize
const end = start + this.shardSize
const packet = this.file.slice(start, end) // 將文件進行切片
let chunkShardSize = this.shardSize
if (this.shardCount === this.index + 1) {
// 最后一片大小
chunkShardSize = this.fileSize - this.index * this.shardSize
}
request({
api: this.api,
headers: {
'Content-Type': 'multipart/form-data'
},
params: {
id: this.fileInfoId,
chunkCount: this.shardCount, // 分片總長度
fileName: this.fileName,
length: this.fileSize,
chunkIndex: this.index + 1, // 當前是第幾片
chunkLength: chunkShardSize, // 當前片大小
file: packet // slice方法用于切出文件的一部分
},
onUploadProgress: (progressEvent) => {
let currentProcessStatus = (progressEvent.loaded / progressEvent.total) * 100 || 0
// 當前分片占整個文件的占比
const chunkShardRatio = chunkShardSize / this.fileSize
if (this.index + 1 < this.shardCount) {
this.progress = chunkShardRatio * this.index * 100 + chunkShardRatio * currentProcessStatus
} else {
// 最后一片
this.progress = (this.shardSize / this.fileSize) * this.index * 100 + chunkShardRatio * currentProcessStatus
}
this.progress = Math.floor(this.progress)
this.onProgress(this.progress)
}
})
.then((res) => {
if (this.index + 1 >= this.shardCount) {
const { fileName, fileId, fileHash, picFileId, picFileHash } = res.data.result || {}
this.isUploading = false
this.onProgress(100)
this.onSuccess({
fileName,
fileId,
fileHash,
picFileId,
picFileHash
})
} else {
this.index < this.shardCount && this.index++
this.uploadHandle()
}
})
.catch((err) => {
console.error('fail', err)
this.onError(err)
})
}
}
調用分片上傳:
// 分片上傳
uploadFpsChunk() {
const chunkUploadInstance = new ChunkUpload({
api: UPLOAD_FPS_CHUNK,
file: this.videoFile,
onProgress: (progress) => {
console.log('上傳進度:', progress)
},
onSuccess: (fileInfo = {}) => {
console.log('分片上傳完畢:', fileInfo)
console.log(fileInfo)
},
onError: (err) => {
console.error('分片上傳失敗:', err)
console.error({ msg: err.msg || '上傳中斷,請檢查網絡' })
}
})
chunkUploadInstance.upload()
},
參考文檔
video標簽特殊屬性詳解:https://blog.csdn.net/web_ding/article/details/112601894
<video>: 視頻嵌入元素:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/video