Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(h5): support chooseMedia api #13859

Merged
merged 13 commits into from
Jun 8, 2023
Merged
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
6 changes: 4 additions & 2 deletions packages/taro-h5/src/api/device/calendar.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Taro from '@tarojs/api'

import { temporarilyNotSupport } from '../../utils'

// 日历
export const addPhoneRepeatCalendar = temporarilyNotSupport('addPhoneRepeatCalendar')
export const addPhoneCalendar = temporarilyNotSupport('addPhoneCalendar')
export const addPhoneRepeatCalendar: typeof Taro.addPhoneRepeatCalendar = temporarilyNotSupport('addPhoneRepeatCalendar')
export const addPhoneCalendar: typeof Taro.addPhoneCalendar = temporarilyNotSupport('addPhoneCalendar')
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)
}
10 changes: 6 additions & 4 deletions packages/taro-h5/src/api/media/image/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { temporarilyNotSupport } from '../../../utils'
import Taro from '@tarojs/api'

import { permanentlyNotSupport, temporarilyNotSupport } from '../../../utils'

// 图片
export const saveImageToPhotosAlbum = temporarilyNotSupport('saveImageToPhotosAlbum')
export const previewMedia = temporarilyNotSupport('previewMedia')
export const saveImageToPhotosAlbum: typeof Taro.saveImageToPhotosAlbum = temporarilyNotSupport('saveImageToPhotosAlbum')
export const previewMedia: typeof Taro.saveImageToPhotosAlbum = temporarilyNotSupport('previewMedia')

export * from './getImageInfo'
export * from './previewImage'

export const compressImage = temporarilyNotSupport('compressImage')
export const chooseMessageFile = temporarilyNotSupport('chooseMessageFile')
export const chooseMessageFile = permanentlyNotSupport('chooseMessageFile')

export * from './chooseImage'

Expand Down
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