React Native 中初始化 FFmpegKit 时出错:“TypeError:无法读取 null 的属性‘getLogLevel’”

问题描述 投票:0回答:1

我正在开发一个 React Native 应用程序,我需要使用 ffmpeg-kit-react-native 库处理视频。但是,我在 FFmpegKitConfig 初始化期间遇到问题。错误信息是:

ERROR  Error initializing FFmpegKit: [TypeError: Cannot read property 'getLogLevel' of null]

这是我的App.js代码

import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Alert, Dimensions, ScrollView, LayoutAnimation, UIManager, Platform } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import * as FileSystem from 'expo-file-system';
import { Video } from 'expo-av';
import { MaterialIcons } from '@expo/vector-icons';
import { FFmpegKit, FFmpegKitConfig, ReturnCode } from 'ffmpeg-kit-react-native';

const windowWidth = Dimensions.get('window').width;

if (Platform.OS === 'android') {
  UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
}

export default function App() {
  const [videoFiles, setVideoFiles] = useState([]);
  const [isGridView, setIsGridView] = useState(false);
  const [isConverting, setIsConverting] = useState(false);

  useEffect(() => {
    FFmpegKitConfig.init()
      .then(() => {
        console.log('FFmpegKit initialized');
      })
      .catch((error) => {
        console.error('Error initializing FFmpegKit:', error);
      });
  }, []);

  const pickVideo = async () => {
    const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
    if (status !== 'granted') {
      alert('Sorry, we need media library permissions to make this work!');
      return;
    }

    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Videos,
      allowsMultipleSelection: true,
    });

    if (!result.canceled && result.assets.length > 0) {
      const newFiles = result.assets.filter(
        (newFile) => !videoFiles.some((existingFile) => existingFile.uri === newFile.uri)
      );

      if (newFiles.length < result.assets.length) {
        Alert.alert('Duplicate Files', 'Some files were already added.');
      }

      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
      setVideoFiles([...videoFiles, ...newFiles]);
    }
  };

  const convertVideos = async () => {
    setIsConverting(true);
    const outputDir = `${FileSystem.documentDirectory}Output`;

    const dirInfo = await FileSystem.getInfoAsync(outputDir);
    if (!dirInfo.exists) {
      await FileSystem.makeDirectoryAsync(outputDir, { intermediates: true });
    }

    for (const video of videoFiles) {
      const { uri } = video;
      const filename = uri.split('/').pop();
      const outputFilePath = `${outputDir}/${filename.split('.').slice(0, -1).join('.')}_modified.mp4`;

      const ffmpegCommand = `-y -i "${uri}" -af "atempo=1.02, bass=g=4:f=80:w=3, treble=g=4:f=3200:w=3, firequalizer=gain_entry='entry(0,0);entry(62,2);entry(125,1.5);entry(250,1);entry(500,1);entry(1000,1);entry(2000,1.5);entry(4000,2.5);entry(8000,3);entry(16000,4)', compand=attacks=0.05:decays=0.25:points=-80/-80-50/-15-30/-10-10/-2:soft-knee=4:gain=2, deesser, highpass=f=35, lowpass=f=17000, loudnorm=I=-16:LRA=11:TP=-1.5, volume=3.9dB" -c:v copy -c:a aac -b:a 224k -ar 48000 -threads 0 "${outputFilePath}"`;

      try {
        const session = await FFmpegKit.execute(ffmpegCommand);
        const returnCode = await session.getReturnCode();

        if (ReturnCode.isSuccess(returnCode)) {
          console.log(`Video converted: ${outputFilePath}`);
        } else if (ReturnCode.isCancel(returnCode)) {
          console.log('Conversion cancelled');
        } else {
          console.error(`FFmpeg process failed: ${session.getFailStackTrace()}`);
        }
      } catch (error) {
        console.error(`Error converting video: ${error.message}`);
      }
    }

    setIsConverting(false);
    Alert.alert('Conversion Complete', 'All videos have been converted.');
  };

  const deleteVideo = (uri) => {
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
    setVideoFiles(videoFiles.filter((video) => video.uri !== uri));
  };

  const clearAllVideos = () => {
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
    setVideoFiles([]);
  };

  const toggleLayout = () => {
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
    setIsGridView(!isGridView);
  };

  return (
    <View style={styles.container}>
      <Text style={styles.header}>Video Converter App</Text>
      <TouchableOpacity style={styles.addButton} onPress={pickVideo}>
        <Text style={styles.addButtonText}>Select or Browse Videos</Text>
      </TouchableOpacity>
      <View style={styles.headerContainer}>
        <Text style={styles.videoCount}>Total Videos: {videoFiles.length}</Text>
        {videoFiles.length > 0 && (
          <>
            <TouchableOpacity onPress={clearAllVideos} style={styles.clearButtonContainer}>
              <MaterialIcons name="clear" size={24} color="red" style={styles.clearIcon} />
              <Text style={styles.clearAllText}>Clear All</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.toggleLayoutButton} onPress={toggleLayout}>
              <MaterialIcons name={isGridView ? 'view-list' : 'view-module'} size={24} color="#fff" />
            </TouchableOpacity>
          </>
        )}
      </View>
      {isGridView ? (
        <ScrollView contentContainerStyle={styles.gridContainer}>
          {videoFiles.map((item, index) => (
            <View key={index} style={styles.videoItemGrid}>
              <Video
                source={{ uri: item.uri }}
                rate={1.0}
                volume={1.0}
                isMuted={true}
                resizeMode="cover"
                shouldPlay={false}
                style={styles.thumbnailGrid}
              />
              <TouchableOpacity onPress={() => deleteVideo(item.uri)} style={styles.deleteButtonGrid}>
                <MaterialIcons name="delete" size={24} color="red" />
              </TouchableOpacity>
            </View>
          ))}
        </ScrollView>
      ) : (
        <View style={styles.list}>
          {videoFiles.map((item, index) => (
            <View key={index} style={styles.videoItem}>
              <Video
                source={{ uri: item.uri }}
                rate={1.0}
                volume={1.0}
                isMuted={true}
                resizeMode="cover"
                shouldPlay={false}
                style={styles.thumbnail}
              />
              <Text style={styles.fileName}>{decodeURI(item.fileName || item.uri.split('/').pop() || 'Unknown File')}</Text>
              <TouchableOpacity onPress={() => deleteVideo(item.uri)} style={styles.deleteButton}>
                <MaterialIcons name="delete" size={24} color="red" />
              </TouchableOpacity>
            </View>
          ))}
        </View>
      )}
      {videoFiles.length > 0 && (
        <TouchableOpacity style={styles.convertButton} onPress={convertVideos} disabled={isConverting}>
          <Text style={styles.convertButtonText}>{isConverting ? 'Converting...' : 'Convert'}</Text>
        </TouchableOpacity>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    padding: 10,
  },
  header: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 5,
  },
  addButton: {
    backgroundColor: '#007BFF',
    padding: 15,
    borderRadius: 10,
    alignItems: 'center',
    marginBottom: 5,
    width: '100%',
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 5 },
    shadowOpacity: 0.8,
    shadowRadius: 2,
  },
  addButtonText: {
    color: '#fff',
    fontSize: 18,
    fontWeight: 'bold',
  },
  headerContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    width: '100%',
    marginBottom: 10,
  },
  videoCount: {
    fontSize: 18,
  },
  clearButtonContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    marginRight: 10,
  },
  clearIcon: {
    marginRight: 5,
  },
  clearAllText: {
    fontSize: 16,
    color: 'red',
    textDecorationLine: 'underline',
  },
  toggleLayoutButton: {
    backgroundColor: '#007BFF',
    padding: 1,
    borderRadius: 8,
    alignItems: 'center',
    justifyContent: 'center',
  },
  list: {
    flex: 1,
    width: '100%',
  },
  videoItem: {
    padding: 5,
    borderBottomColor: '#ccc',
    borderBottomWidth: 0.7,
    flexDirection: 'row',
    alignItems: 'center',
  },
  videoItemGrid: {
    flexDirection: 'column',
    alignItems: 'center',
    margin: 4,
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 2,
    position: 'relative',
  },
  thumbnail: {
    width: 70,
    height: 70,
    marginRight: 10,
  },
  thumbnailGrid: {
    width: 80,
    height: 80,
    marginBottom: 0,
  },
  fileName: {
    fontSize: 16,
    marginLeft: 10,
    flex: 1,
  },
  deleteButton: {
    marginLeft: 60,
    width: 20,
    height: 20,
  },
  deleteButtonGrid: {
    position: 'absolute',
    bottom: 5,
    right: 5,
  },
  convertButton: {
    backgroundColor: '#007BFF',
    padding: 15,
    borderRadius: 10,
    alignItems: 'center',
    marginTop: 20,
    width: '100%',
  },
  convertButtonText: {
    color: '#fff',
    fontSize: 18,
    fontWeight: 'bold',
  },
  gridContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    justifyContent: 'flex-start',
    paddingVertical: 5,
    paddingHorizontal: 5,
    width: '100%',
  },
});

App.json

{
  "expo": {
    "name": "VidoeConvert",
    "slug": "VidoeConvert",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "light",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "ios": {
      "supportsTablet": true
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.anonymous.VidoeConvert"
    },
    "web": {
      "favicon": "./assets/favicon.png"
    },
    "plugins": [
      "@config-plugins/ffmpeg-kit-react-native",
      "expo-build-properties"
    ]
  }
}

Package.json

{
  "name": "vidoeconvert",
  "version": "1.0.0",
  "main": "expo/AppEntry.js",
  "scripts": {
    "start": "expo start",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
    "@expo/metro-runtime": "~3.2.1",
    "expo": "~51.0.17",
    "expo-asset": "~10.0.10",
    "expo-av": "^14.0.6",
    "expo-document-picker": "~12.0.2",
    "expo-file-system": "~17.0.1",
    "expo-image-picker": "~15.0.7",
    "expo-media-library": "~16.0.4",
    "expo-status-bar": "~1.12.1",
    "ffmpeg-kit-react-native": "^6.0.2",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "^0.74.3",
    "react-native-document-picker": "^9.3.0",
    "react-native-ffmpeg": "^0.5.2",
    "react-native-vector-icons": "^10.1.0",
    "react-native-web": "~0.19.10",
    "expo-build-properties": "~0.12.3"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0"
  },
  "private": true
}

有没有人遇到过类似的问题,或者可以为我指出解决此错误的正确方向?任何帮助将不胜感激!

如何消除错误?

这个项目需要什么配置吗?

react-native ffmpeg typeerror android-ffmpeg
1个回答
0
投票

我这几天也遇到同样的问题。你找到解决办法了吗?

© www.soinside.com 2019 - 2024. All rights reserved.