Permitir Vídeo no Promoter
Ticket: PROMOTER-432 > Última atualização: 30 de julho de 2024
Visão Geral
Implementação de funcionalidade para permitir a captura, armazenamento e sincronização de vídeos no aplicativo Promoter, possibilitando que os usuários registrem vídeos durante as atividades de campo, tanto para o cenário "antes" quanto para o cenário "depois" das atividades. A solução contempla todo o fluxo desde a captura do vídeo até o envio para o servidor, incluindo o armazenamento offline e a sincronização posterior.
Tipos de Mídia Suportados
| Tipo | Formato | Descrição |
|---|---|---|
| Imagem | JPEG | Fotos da fachada, antes e depois |
| Vídeo | MP4 | Vídeos do antes e depois das atividades |
Configuração de Campos
Campos na Configuração (Settings)
| Campo | Descrição | Tabela | Uso |
|---|---|---|---|
UTILIZAVIDEO | Habilita uso de vídeo no aplicativo | Settings | Define se o recurso de vídeo está disponível |
Arquivos Modificados
| Arquivo | Localização | Responsabilidade |
|---|---|---|
attachments.ts | database/schemas | Schema para armazenamento de vídeos |
records.ts | database/schemas | Schema para registros com vídeos |
GetSettingsServiceResponseDTO.ts | dtos/User | Interface para configuração de vídeo |
ActivityType.ts | interfaces | Interfaces para tipos de atividade |
VideoCard/index.tsx | screens/ActivityNote/components | Componente para exibição de vídeos |
VideoModal/index.tsx | screens/ActivityNote/components | Modal para reprodução de vídeos |
checkinTabController.ts | screens/ActivityNote/subscreens/Checkin | Controlador para captura de vídeos |
index.tsx (Checkin) | screens/ActivityNote/subscreens/Checkin | Interface para captura de vídeos |
index.tsx (Checkout) | screens/ActivityNote/subscreens/Checkout | Interface para finalização com vídeos |
ShareBottomSheet/index.tsx | screens/Records/components/RecordOfflineCard | Compartilhamento de vídeos |
index.tsx (ServiceReport) | screens/ServiceReport | Exibição de vídeos em relatórios |
sendVideoAttachment.ts | services/Activity | Serviço para envio de vídeos |
convertToSend.ts | services/backgroundTasks | Conversão de vídeos para envio |
downloadRecordVideoService.ts | services/Records | Serviço para download de vídeos |
index.ts (syncService) | services/syncService | Sincronização de vídeos |
settingsSlice.ts | store/settings | Redux para configuração de vídeos |
settingsTypes.ts | store/settings | Tipos para configuração de vídeos |
getVideoFilePathFromBase64.ts | utils | Utilitário para manipulação de vídeos |
Fluxo de Funcionamento
Fluxograma do Processo
Componentes Principais
1. Captura de Vídeo
A captura de vídeo é realizada através do componente Camera da biblioteca react-native-vision-camera, que permite gravar vídeos com controle de qualidade e formato. O processo é gerenciado pelo checkinTabController.ts, que controla o início e fim da gravação, bem como o processamento do vídeo capturado.
// Exemplo de código para iniciar gravação
const handleRecordVideo = async () => {
try {
setIsRecording(true);
const videoId = generateUUID();
await cameraRef.current.startRecording({
fileType: Platform.OS === "ios" ? "mov" : "mp4",
flash: "off",
onRecordingFinished: async (video) => {
// Processamento do vídeo após finalização
const base64 = await FileSystem.readAsStringAsync(video.path, {
encoding: FileSystem.EncodingType.Base64,
});
// Salvar vídeo localmente
savePhotoToDatabase(videoId, base64, true);
// Atualizar estado do aplicativo
dispatch(
setCheckin({
attachments: [
...checkin?.attachments,
{
isNew: true,
type: currentType,
value: value.toString(),
createdAt: new Date(),
isLoading: "success",
id: videoId,
isVideo: true,
},
],
})
);
},
});
} catch (error) {
console.error("Erro ao iniciar gravação:", error);
}
};
2. Armazenamento Local
Os vídeos são armazenados localmente em formato Base64 utilizando o schema attachmentImage, que foi adaptado para suportar vídeos através da adição do campo ISVIDEO:
export const attachmentImage: Realm.ObjectSchema = {
name: "attachmentImage",
primaryKey: "ID",
properties: {
ID: "string",
BASE64: "string",
ISVIDEO: "bool",
},
};
3. Sincronização
A sincronização dos vídeos é realizada pelo serviço syncService, que foi adaptado para identificar e enviar vídeos para o servidor:
// Trecho do código de sincronização de vídeos
if (
response &&
response.data?.NUPRM &&
((VIDEOSANTES && VIDEOSANTES.length > 0) ||
(VIDEOSDEPOIS && VIDEOSDEPOIS.length > 0))
) {
// Processar vídeos do "antes"
for (const objImage of VIDEOSANTES) {
const media = getCurrentImageFromCache(objImage.id);
const path = await getVideoFilePathFromBase64(
media.BASE64,
objImage.id,
"before"
);
// Criar FormData para upload
const formData = new FormData();
formData.append("file", {
uri: path,
name: `video_antes_${Date.now()}.mp4`,
type: "video/mp4",
} as any);
// Enviar para o servidor
await uploadVideoAttachment(response.data?.NUPRM, formData);
}
// Processar vídeos do "depois" (código similar)
}
4. Exibição
A exibição dos vídeos é realizada através dos componentes VideoCard e VideoModal, que permitem visualizar miniaturas dos vídeos e reproduzi-los em tela cheia:
// VideoCard - Miniatura do vídeo
export const VideoCard = ({ base64Uri, id, onPress, currentType }) => {
const [videoPath, setVideoPath] = useState("");
useEffect(() => {
let active = true;
(async () => {
if (base64Uri?.startsWith("data:video/mp4;base64,")) {
const base64 = base64Uri.replace("data:video/mp4;base64,", "");
const path = await getVideoFilePathFromBase64(base64, id, currentType);
if (active) setVideoPath(path);
} else if (base64Uri) {
setVideoPath(base64Uri);
}
})();
return () => {
active = false;
};
}, [base64Uri, id]);
return (
<TouchableOpacity onPress={() => onPress(videoPath)} activeOpacity={0.8}>
<View
style={
{
/* estilos */
}
}
>
<Video source={{ uri: videoPath }} paused resizeMode="cover" />
</View>
<View
style={
{
/* estilos */
}
}
>
<Play color="white" weight="bold" size={20} />
</View>
</TouchableOpacity>
);
};
Configuração do Recurso
Habilitando o Recurso
O recurso de vídeo pode ser habilitado ou desabilitado através da configuração UTILIZAVIDEO no painel administrativo. Quando habilitado, a interface do aplicativo exibirá a opção de gravação de vídeo em vez da captura de fotos.
Configuração no Backend
No backend, é necessário garantir que o servidor esteja configurado para receber e processar arquivos de vídeo. A API /note/${NUPRM}/saveVideo foi implementada para receber os vídeos enviados pelo aplicativo.
Permissões Necessárias
O aplicativo requer as seguintes permissões para o funcionamento correto da funcionalidade de vídeo:
-
Android:
"permissions": [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"
] -
iOS:
<key>NSCameraUsageDescription</key>
<string>Vidya Promoter necessita de acesso a sua camera.</string>
Implementação Técnica
Modificações no Schema do Banco de Dados
O banco de dados local foi adaptado para suportar o armazenamento de vídeos:
-
Schema de Anexos:
export const attachmentImage: Realm.ObjectSchema = {
name: "attachmentImage",
primaryKey: "ID",
properties: {
ID: "string",
BASE64: "string",
ISVIDEO: "bool", // Novo campo para identificar vídeos
},
}; -
Schema de Registros:
export const recordVideosData: Realm.ObjectSchema = {
name: "recordVideosData",
properties: {
NUVIDEO: "string?",
SIZE: "double?",
FILENAME: "string?",
TYPE: "string?",
},
};
Modificações na Interface
A interface do usuário foi adaptada para suportar a captura e visualização de vídeos:
-
Botão de Captura:
<Pressable
onPress={() => controller.getImageFromCamera("before")}
w={10}
h={10}
rounded="full"
alignItems="center"
justifyContent="center"
bgColor="#444444"
>
{UTILIZAVIDEO === "S" ? (
<VideoCamera color="white" weight="bold" size={20} />
) : (
<Camera color="white" weight="bold" size={20} />
)}
</Pressable> -
Indicador de Gravação:
{
isRecording && (
<Animated.View style={[styles.recordingIndicator, animatedStyle]}>
<Text style={styles.recordingText}>{formatTime(elapsedTime)}</Text>
</Animated.View>
);
}
Serviços de API
Foram implementados serviços para envio e download de vídeos:
-
Envio de Vídeos:
const uploadVideoAttachment = async (NUPRM: string, formData: FormData) => {
try {
const response = await api.post(`/note/${NUPRM}/saveVideo`, formData);
return response;
} catch (error) {
console.log("❌ Erro ao enviar vídeo:", error);
throw error;
}
}; -
Download de Vídeos:
const downloadRecordVideoService = async (
NUPRM: string,
body: { FILENAME: string; NUVIDEO: string }
) => {
try {
const response = await api.post(`/note/${NUPRM}/downloadVideo`, body, {
responseType: "arraybuffer",
});
return response;
} catch (error) {
console.log("erro no downloadRecordVideoService", error);
}
};
Considerações de Performance
Otimização de Armazenamento
Para otimizar o armazenamento de vídeos, foram implementadas as seguintes estratégias:
- Compressão de Vídeo: Os vídeos são comprimidos antes do armazenamento local.
- Limpeza de Cache: Os arquivos temporários são removidos após o envio bem-sucedido para o servidor.
Otimização de Rede
Para otimizar o uso de rede durante a sincronização, foram implementadas as seguintes estratégias:
- Sincronização Seletiva: Apenas os vídeos não sincronizados são enviados para o servidor.
- Retry Mechanism: Em caso de falha no envio, o sistema tenta novamente automaticamente.
Limitações e Considerações Futuras
Limitações Atuais
- Tamanho do Vídeo: Não há limite explícito para o tamanho dos vídeos, o que pode causar problemas de armazenamento.
- Duração da Gravação: Não há limite para a duração da gravação, o que pode resultar em arquivos muito grandes.
Melhorias Futuras
- Limite de Duração: Implementar limite de duração para gravações de vídeo.
- Compressão Avançada: Implementar algoritmos de compressão mais eficientes.
- Streaming de Vídeo: Implementar streaming para visualização de vídeos grandes sem necessidade de download completo.
Exemplos Práticos
Cenário: Registro de Atividade com Vídeo
-
Configuração:
UTILIZAVIDEO= 'S'
-
Fluxo do Usuário:
- Usuário inicia um novo registro de atividade
- Seleciona a opção "Registro Antes"
- Toca no botão de vídeo para iniciar a gravação
- Grava o cenário atual
- Finaliza a gravação
- O vídeo é processado e exibido como miniatura
- Usuário pode visualizar o vídeo tocando na miniatura
- Após concluir a atividade, repete o processo para "Registro Depois"
- Finaliza o registro e o sistema sincroniza os vídeos com o servidor quando houver conexão
Exemplo de Código para Reprodução de Vídeo
export default function VideoModal({
visible,
onClose,
videoUri,
handleDelete,
}) {
const [paused, setPaused] = useState(false);
return (
<Modal visible={visible} transparent animationType="fade">
<Pressable
style={{ flex: 1, backgroundColor: "#000" }}
onPress={() => setPaused(!paused)}
>
<Video
source={{ uri: videoUri }}
style={{ flex: 1 }}
resizeMode="contain"
paused={paused}
controls
ignoreSilentSwitch="ignore"
onEnd={() => onClose()}
/>
{/* Botões de controle */}
<TouchableOpacity
style={
{
/* estilos */
}
}
onPress={onClose}
/>
{handleDelete && (
<TouchableOpacity
style={
{
/* estilos */
}
}
onPress={() => {
onClose();
showWarningOrConfirmationModal({
title: "Atenção!",
type: "error",
subtitle: "Tem certeza que quer deletar esse video?",
onConfirm: handleDelete,
});
}}
>
<Feather name="trash-2" size={24} color="#fff" />
</TouchableOpacity>
)}
</Pressable>
</Modal>
);
}
Observações
- A implementação de vídeo é compatível com dispositivos Android e iOS.
- O recurso pode ser habilitado ou desabilitado remotamente através da configuração
UTILIZAVIDEO. - Os vídeos são armazenados localmente e sincronizados com o servidor quando houver conexão disponível.
- A interface do usuário adapta-se automaticamente para exibir a opção de vídeo ou foto, dependendo da configuração.