Skip to content

Commit

Permalink
feat: add video recording support for android
Browse files Browse the repository at this point in the history
  • Loading branch information
khushal87 committed Jan 3, 2025
1 parent 9d02d86 commit 521ea88
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 95 deletions.
96 changes: 53 additions & 43 deletions package/expo-package/src/optionalDependencies/takePhoto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ type Size = {
width?: number;
};

// Media type mapping for iOS and Android
const mediaTypeMap = {
mixed: ['images', 'videos'],
image: 'images',
video: 'videos',
};

export const takePhoto = ImagePicker
? async ({ compressImageQuality = 1 }) => {
? async ({ compressImageQuality = 1, mediaType = Platform.OS === 'ios' ? 'mixed' : 'image' }) => {
try {
const permissionCheck = await ImagePicker.getCameraPermissionsAsync();
const canRequest = permissionCheck.canAskAgain;
Expand All @@ -36,61 +43,64 @@ export const takePhoto = ImagePicker

if (permissionGranted) {
const imagePickerSuccessResult = await ImagePicker.launchCameraAsync({
mediaTypes: Platform.OS === 'ios' ? ['images', 'videos'] : 'images',
mediaTypes: Platform.OS === 'ios' ? mediaTypeMap[mediaType] : mediaType[mediaType],
quality: Math.min(Math.max(0, compressImageQuality), 1),
});
const canceled = imagePickerSuccessResult.canceled;
const assets = imagePickerSuccessResult.assets;
// since we only support single photo upload for now we will only be focusing on 0'th element.
const photo = assets && assets[0];
if (Platform.OS === 'ios') {
if (photo.mimeType.includes('video')) {
const clearFilter = new RegExp('[.:]', 'g');
const date = new Date().toISOString().replace(clearFilter, '_');
return {
...photo,
cancelled: false,
duration: photo.duration,
source: 'camera',
name: 'video_recording_' + date + photo.uri.split('.').pop(),
size: photo.fileSize,
type: photo.mimeType,
uri: photo.uri,
};
}
console.log('photo', photo);
if (canceled) {
return { cancelled: true };
}
if (canceled === false && photo && photo.height && photo.width && photo.uri) {
let size: Size = {};
if (Platform.OS === 'android') {
const getSize = (): Promise<Size> =>
new Promise((resolve) => {
Image.getSize(photo.uri, (width, height) => {
resolve({ height, width });
});
});

try {
const { height, width } = await getSize();
size.height = height;
size.width = width;
} catch (e) {
console.warn('Error get image size of picture caputred from camera ', e);
}
} else {
size = {
height: photo.height,
width: photo.width,
};
}

if (photo.mimeType.includes('video')) {
const clearFilter = new RegExp('[.:]', 'g');
const date = new Date().toISOString().replace(clearFilter, '_');
return {
...photo,
cancelled: false,
duration: photo.duration, // in milliseconds
source: 'camera',
name: 'video_recording_' + date + photo.uri.split('.').pop(),
size: photo.fileSize,
type: photo.mimeType,
source: 'camera',
uri: photo.uri,
...size,
};
} else {
if (photo && photo.height && photo.width && photo.uri) {
let size: Size = {};
if (Platform.OS === 'android') {
const getSize = (): Promise<Size> =>
new Promise((resolve) => {
Image.getSize(photo.uri, (width, height) => {
resolve({ height, width });
});
});

try {
const { height, width } = await getSize();
size.height = height;
size.width = width;
} catch (e) {
console.warn('Error get image size of picture caputred from camera ', e);
}
} else {
size = {
height: photo.height,
width: photo.width,
};
}

return {
cancelled: false,
size: photo.fileSize,
type: photo.mimeType,
source: 'camera',
uri: photo.uri,
...size,
};
}
}
}
} catch (error) {
Expand Down
92 changes: 47 additions & 45 deletions package/native-package/src/optionalDependencies/takePhoto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ try {
}

export const takePhoto = ImagePicker
? async ({ compressImageQuality = Platform.OS === 'ios' ? 0.8 : 1 }) => {
? async ({
compressImageQuality = Platform.OS === 'ios' ? 0.8 : 1,
mediaType = Platform.OS === 'ios' ? 'mixed' : 'image',
}) => {
if (Platform.OS === 'android') {
const cameraPermissions = await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.CAMERA,
Expand All @@ -29,7 +32,7 @@ export const takePhoto = ImagePicker
}
try {
const result = await ImagePicker.launchCamera({
mediaType: Platform.OS === 'ios' ? 'mixed' : 'images',
mediaType: mediaType,
quality: Math.min(Math.max(0, compressImageQuality), 1),
});
if (!result.assets.length) {
Expand All @@ -38,56 +41,55 @@ export const takePhoto = ImagePicker
};
}
const asset = result.assets[0];
if (Platform.OS === 'ios') {
if (asset.type.includes('video')) {
const clearFilter = new RegExp('[.:]', 'g');
const date = new Date().toISOString().replace(clearFilter, '_');
if (asset.type.includes('video')) {
const clearFilter = new RegExp('[.:]', 'g');
const date = new Date().toISOString().replace(clearFilter, '_');
return {
...asset,
cancelled: false,
duration: asset.duration * 1000,
source: 'camera',
name: 'video_recording_' + date + asset.fileName.split('.').pop(),
size: asset.fileSize,
type: asset.type,
uri: asset.uri,
};
} else {
if (asset.height && asset.width && asset.uri) {
let size: { height?: number; width?: number } = {};
if (Platform.OS === 'android') {
// Height and width returned by ImagePicker are incorrect on Android.
const getSize = (): Promise<{ height: number; width: number }> =>
new Promise((resolve) => {
Image.getSize(asset.uri, (width, height) => {
resolve({ height, width });
});
});

try {
const { height, width } = await getSize();
size.height = height;
size.width = width;
} catch (e) {
// do nothing
console.warn('Error get image size of picture caputred from camera ', e);
}
} else {
size = {
height: asset.height,
width: asset.width,
};
}
return {
...asset,
cancelled: false,
duration: asset.duration ? asset.duration * 1000 : undefined, // in milliseconds
source: 'camera',
name: 'video_recording_' + date + asset.fileName.split('.').pop(),
size: asset.fileSize,
type: asset.type,
size: asset.size,
source: 'camera',
uri: asset.uri,
...size,
};
}
}
if (asset.height && asset.width && asset.uri) {
let size: { height?: number; width?: number } = {};
if (Platform.OS === 'android') {
// Height and width returned by ImagePicker are incorrect on Android.
const getSize = (): Promise<{ height: number; width: number }> =>
new Promise((resolve) => {
Image.getSize(asset.uri, (width, height) => {
resolve({ height, width });
});
});

try {
const { height, width } = await getSize();
size.height = height;
size.width = width;
} catch (e) {
// do nothing
console.warn('Error get image size of picture caputred from camera ', e);
}
} else {
size = {
height: asset.height,
width: asset.width,
};
}
return {
cancelled: false,
type: asset.type,
size: asset.size,
source: 'camera',
uri: asset.uri,
...size,
};
}
} catch (e: unknown) {
if (e instanceof Error) {
// on iOS: if it was in inactive state, then the user had just denied the permissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useMessageInputContext } from '../../../contexts/messageInputContext/Me
import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext';
import { useOwnCapabilitiesContext } from '../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { Recorder } from '../../../icons';

const styles = StyleSheet.create({
container: {
Expand Down Expand Up @@ -48,6 +49,7 @@ export const AttachmentPickerSelectionBar = () => {
const {
theme: {
attachmentSelectionBar: { container, icon },
colors: { grey },
},
} = useTheme();

Expand Down Expand Up @@ -105,7 +107,9 @@ export const AttachmentPickerSelectionBar = () => {
{hasCameraPicker ? (
<TouchableOpacity
hitSlop={{ bottom: 15, top: 15 }}
onPress={takeAndUploadImage}
onPress={() => {
takeAndUploadImage();
}}
testID='take-photo-touchable'
>
<View style={[styles.icon, icon]}>
Expand All @@ -116,6 +120,19 @@ export const AttachmentPickerSelectionBar = () => {
</View>
</TouchableOpacity>
) : null}
{hasCameraPicker ? (
<TouchableOpacity
hitSlop={{ bottom: 15, top: 15 }}
onPress={() => {
takeAndUploadImage('video');
}}
testID='take-photo-touchable'
>
<View style={[styles.icon, icon]}>
<Recorder pathFill={grey} height={20} width={20} />
</View>
</TouchableOpacity>
) : null}
{!threadList && hasCreatePoll && ownCapabilities.sendPoll ? ( // do not allow poll creation in threads
<TouchableOpacity
hitSlop={{ bottom: 15, top: 15 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CameraSelectorIcon } from '../../AttachmentPicker/components/CameraSele
import { FileSelectorIcon } from '../../AttachmentPicker/components/FileSelectorIcon';
import { ImageSelectorIcon } from '../../AttachmentPicker/components/ImageSelectorIcon';
import { CreatePollIcon } from '../../Poll/components/CreatePollIcon';
import { Recorder } from '../../../icons';

type NativeAttachmentPickerProps = {
onRequestedClose: () => void;
Expand All @@ -28,10 +29,10 @@ export const NativeAttachmentPicker = ({
}: NativeAttachmentPickerProps) => {
const size = attachButtonLayoutRectangle?.width ?? 0;
const attachButtonItemSize = 40;
const NUMBER_OF_BUTTONS = 3;
const NUMBER_OF_BUTTONS = 5;
const {
theme: {
colors: { grey_whisper },
colors: { grey, grey_whisper },
messageInput: {
nativeAttachmentPicker: {
buttonContainer,
Expand Down Expand Up @@ -149,6 +150,13 @@ export const NativeAttachmentPicker = ({
id: 'Camera',
onPressHandler: takeAndUploadImage,
});
buttons.push({
icon: <Recorder pathFill={grey} height={20} width={20} />,
id: 'Video',
onPressHandler: () => {
takeAndUploadImage('video');
},
});
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import type { Emoji } from '../../emoji-data';
import {
isDocumentPickerAvailable,
isImageMediaLibraryAvailable,
MediaTypes,
pickDocument,
pickImage,
takePhoto,
Expand Down Expand Up @@ -232,7 +233,7 @@ export type LocalMessageInputContext<
/**
* Function for taking a photo and uploading it
*/
takeAndUploadImage: () => Promise<void>;
takeAndUploadImage: (mediaType?: MediaTypes) => Promise<void>;
text: string;
toggleAttachmentPicker: () => void;
/**
Expand Down Expand Up @@ -686,10 +687,10 @@ export const MessageInputProvider = <
/**
* Function for capturing a photo and uploading it
*/
const takeAndUploadImage = async () => {
const takeAndUploadImage = async (mediaType?: MediaTypes) => {
setSelectedPicker(undefined);
closePicker();
const photo = await takePhoto({ compressImageQuality: value.compressImageQuality });
const photo = await takePhoto({ compressImageQuality: value.compressImageQuality, mediaType });
if (photo.askToOpenSettings) {
Alert.alert(
t('Allow camera access in device settings'),
Expand Down
6 changes: 5 additions & 1 deletion package/src/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ type Photo = Omit<Asset, 'source'> & {
askToOpenSettings?: boolean;
cancelled?: boolean;
};
type TakePhoto = (options: { compressImageQuality?: number }) => Promise<Photo> | never;
export type MediaTypes = 'image' | 'video' | 'mixed';
type TakePhoto = (options: {
compressImageQuality?: number;
mediaType?: MediaTypes;
}) => Promise<Photo> | never;
export let takePhoto: TakePhoto = fail;

type HapticFeedbackMethod =
Expand Down

0 comments on commit 521ea88

Please sign in to comment.