Skip to content

Commit

Permalink
Merge pull request #13859 from NervJS/fix/h5_media
Browse files Browse the repository at this point in the history
feat(h5): support chooseMedia api
  • Loading branch information
ZakaryCode authored Jun 8, 2023
2 parents 564742f + c4a9181 commit f7696a5
Show file tree
Hide file tree
Showing 11 changed files with 470 additions and 173 deletions.
2 changes: 1 addition & 1 deletion packages/taro-components/src/components/video/video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ export class Video implements ComponentInterface {

if (isHls(src)) {
import(
/* webpackMode: "weak" */
/* webpackExports: ["default"] */
'hls.js'
).then(e => {
const Hls = e.default
Expand Down
106 changes: 36 additions & 70 deletions packages/taro-h5/src/api/media/image/chooseImage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Taro from '@tarojs/api'

import { getParameterError, shouldBeObject } from '../../../utils'
import { MethodHandler } from '../../../utils/handler'
import { shouldBeObject } from '../../../utils'
import { chooseMedia } from '../video/chooseMedia'

/**
* 从本地相册选择图片或使用相机拍照。
* @deprecated 请使用 chooseMedia 接口
*/
export const chooseImage: typeof Taro.chooseImage = function (options) {
// options must be an Object
Expand All @@ -15,81 +16,46 @@ export const chooseImage: typeof Taro.chooseImage = function (options) {
return Promise.reject(res)
}

let camera = 'back'
const {
count = 1,
sourceType = ['album', 'camera'],
success,
fail,
complete,
imageId = 'taroChooseImage',
sourceType = ['album', 'camera']
fail,
...args
} = options
const handle = new MethodHandler({ name: 'chooseImage', success, fail, complete })
const res: Partial<Taro.chooseImage.SuccessCallbackResult> = {
tempFilePaths: [],
tempFiles: []
}
const sourceTypeString = sourceType && sourceType.toString()
const acceptableSourceType = ['user', 'environment', 'camera']

if (count && typeof count !== 'number') {
res.errMsg = getParameterError({
para: 'count',
correct: 'Number',
wrong: count
})
return handle.fail(res)
if (sourceType.includes('camera') && sourceType.indexOf('user') > -1) {
camera = 'front'
}

let el = document.getElementById(imageId)
if (!el) {
const obj = document.createElement('input')
obj.setAttribute('type', 'file')
obj.setAttribute('id', imageId)
if (count > 1) {
obj.setAttribute('multiple', 'multiple')
}
if (acceptableSourceType.indexOf(sourceTypeString) > -1) {
obj.setAttribute('capture', sourceTypeString)
}
obj.setAttribute('accept', 'image/*')
obj.setAttribute('style', 'position: fixed; top: -4000px; left: -3000px; z-index: -300;')
document.body.appendChild(obj)
el = document.getElementById(imageId)
} else {
if (count > 1) {
el.setAttribute('multiple', 'multiple')
} else {
el.removeAttribute('multiple')
}
if (acceptableSourceType.indexOf(sourceTypeString) > -1) {
el.setAttribute('capture', sourceTypeString)
} else {
el.removeAttribute('capture')
function parseRes (res: Taro.chooseMedia.SuccessCallbackResult): Taro.chooseImage.SuccessCallbackResult {
const { tempFiles = [], errMsg } = res
return {
tempFilePaths: tempFiles.map(item => item.tempFilePath),
tempFiles: tempFiles.map(item => ({
path: item.tempFilePath,
size: item.size,
type: item.fileType,
originalFileObj: item.originalFileObj,
})),
errMsg,
}
}

return new Promise((resolve, reject) => {
const TaroMouseEvents = document.createEvent('MouseEvents')
TaroMouseEvents.initEvent('click', true, true)
if (el) {
el.dispatchEvent(TaroMouseEvents)
el.onchange = function (e) {
const target = e.target as HTMLInputElement
if (target) {
const files = target.files || []
const arr = [...files]
arr && arr.forEach(item => {
const blob = new Blob([item], {
type: item.type
})
const url = URL.createObjectURL(blob)
res.tempFilePaths?.push(url)
res.tempFiles?.push({ path: url, size: item.size, type: item.type, originalFileObj: item })
})
handle.success(res, { resolve, reject })
target.value = ''
}
}
}
})
return chooseMedia({
mediaId: 'taroChooseImage',
...args,
sourceType: sourceType as Taro.chooseMedia.Option['sourceType'],
mediaType: ['image'],
camera,
success: (res) => {
const param = parseRes(res)
success?.(param)
complete?.(param)
},
fail: (err) => {
fail?.(err)
complete?.(err)
},
}, 'chooseImage').then(parseRes)
}
213 changes: 213 additions & 0 deletions packages/taro-h5/src/api/media/video/chooseMedia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import Taro from '@tarojs/api'
import { getMobileDetect } from '@tarojs/router/dist/utils/navigate'

import { showActionSheet } from '../../../api/ui'
import { getParameterError, shouldBeObject } from '../../../utils'
import { MethodHandler } from '../../../utils/handler'

/**
* 拍摄或从手机相册中选择图片或视频。
*/
export const chooseMedia = async function (
options: Taro.chooseMedia.Option,
methodName = 'chooseMedia',
): Promise<Taro.chooseMedia.SuccessCallbackResult> {
// options must be an Object
const isObject = shouldBeObject(options)
if (!isObject.flag) {
const res = { errMsg: `${methodName}:fail ${isObject.msg}` }
console.error(res.errMsg)
return Promise.reject(res)
}

const {
count = 9,
mediaId = 'taroChooseMedia',
mediaType = ['image', 'video'],
sourceType = ['album', 'camera'],
// sizeType = ['original', 'compressed'], // TODO 考虑通过 ffmpeg 支持压缩
// maxDuration = 10, // TODO 考虑通过 ffmpeg 剪裁视频
camera = 'back',
success,
fail,
complete,
} = options
const handle = new MethodHandler({ name: methodName, success, fail, complete })
const withImage = mediaType.length < 1 || mediaType.indexOf('image') > -1
const withVideo = mediaType.length < 1 || mediaType.indexOf('video') > -1
const res: Partial<Taro.chooseMedia.SuccessCallbackResult> = {
tempFiles: [],
type: withImage && withVideo ? 'mix' : withImage ? 'image' : 'video',
}

if (count && typeof count !== 'number') {
res.errMsg = getParameterError({
para: 'count',
correct: 'Number',
wrong: count
})
return handle.fail(res)
}

let el = document.getElementById(mediaId)
if (!el) {
el = document.createElement('input')
el.setAttribute('type', 'file')
el.setAttribute('id', mediaId)
el.setAttribute('style', 'position: fixed; top: -4000px; left: -3000px; z-index: -300;')
}

if (count > 1) {
el.setAttribute('multiple', 'multiple')
} else {
el.removeAttribute('multiple')
}

// Note: Input 仅在移动端支持 capture 属性,可以使用 getUserMedia 替代(暂不考虑)
const md = getMobileDetect()
if (md.mobile()) {
if (sourceType.length > 1 || sourceType.length < 1) {
try {
const { tapIndex } = await showActionSheet({
itemList: ['拍摄', '从相册选择'],
}, methodName)
sourceType.splice(0, 1, tapIndex === 0 ? 'camera' : 'album')
} catch (e) {
return handle.fail({
errMsg: e.errMsg?.replace('^.*:fail ', '')
})
}
}
}
if (sourceType.includes('camera')) {
el.setAttribute('capture', camera === 'front' ? 'user' : 'environment')
} else {
el.removeAttribute('capture')
}

if (res.type === 'image') {
el.setAttribute('accept', 'image/*')
} else if (res.type === 'video') {
el.setAttribute('accept', 'video/*')
} else {
el.setAttribute('accept', 'image/*, video/*')
}
return new Promise<Taro.chooseMedia.SuccessCallbackResult>((resolve, reject) => {
if (!el) return
document.body.appendChild(el)
el.onchange = async function (e) {
const target = e.target as HTMLInputElement
if (target) {
const files = target.files || []
const arr = [...files]
await Promise.all(
arr.map(async item => {
try {
res.tempFiles?.push(await loadMedia(item))
} catch (error) {
console.error(error)
}
})
)
}
handle.success(res, { resolve, reject })
target.value = ''
}
el.onabort = () => handle.fail({ errMsg: 'abort' }, { resolve, reject })
el.oncancel = () => handle.fail({ errMsg: 'cancel' }, { resolve, reject })
el.onerror = e => handle.fail({ errMsg: e.toString() }, { resolve, reject })
el.click()
}).finally(() => {
if (!el) return
document.body.removeChild(el)
})

function loadMedia (file: File): Promise<Taro.chooseMedia.ChooseMedia> {
const dataUrl = URL.createObjectURL(file)
const res = {
tempFilePath: dataUrl,
size: file.size,
duration: 0,
height: 0,
width: 0,
thumbTempFilePath: '',
fileType: file.type,
originalFileObj: file
}

if (/^video\//.test(res.fileType)) {
// Video
const video = document.createElement('video')
const reader = new FileReader()
video.crossOrigin = 'Anonymous'
video.preload = 'metadata'
video.src = res.tempFilePath

return new Promise((resolve, reject) => {
// 对齐旧版本实现
reader.onload = (event) => {
res.tempFilePath = event.target?.result as string
}
reader.onerror = e => reject(e)
reader.readAsDataURL(res.originalFileObj)

video.onloadedmetadata = () => {
res.duration = video.duration
res.height = video.videoHeight
res.width = video.videoWidth
}
video.oncanplay = () => {
res.thumbTempFilePath = getThumbTempFilePath(video, res.height, res.width, 0.8)
resolve(res)
}
video.onerror = e => reject(e)
})
} else {
// Image
const img = new Image()
/** 允许图片和 canvas 跨源使用
* https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_enabled_image
*/
img.crossOrigin = 'Anonymous'
img.src = res.tempFilePath

return new Promise((resolve, reject) => {
if (img.complete) {
res.height = img.height
res.width = img.width
res.thumbTempFilePath = getThumbTempFilePath(img, res.height, res.width, 0.8)
resolve(res)
} else {
img.onload = () => {
res.height = img.height
res.width = img.width
res.thumbTempFilePath = getThumbTempFilePath(img, res.height, res.width, 0.8)
resolve(res)
}
img.onerror = e => reject(e)
}
})
}
}

function getThumbTempFilePath (el: CanvasImageSource, height = 0, width = height, quality = 0.8) {
const max = 256
const canvas = document.createElement('canvas')
if (height > max || width > max) {
const radio = height / width
if (radio > 1) {
height = max
width = height / radio
} else {
width = max
height = width * radio
}
}
canvas.height = height
canvas.width = width

const ctx = canvas.getContext('2d')
ctx?.drawImage(el, 0, 0, canvas.width, canvas.height)
return canvas.toDataURL('image/jpeg', quality)
}
}
Loading

0 comments on commit f7696a5

Please sign in to comment.