You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo AV实现后台音频播放问题求助

后台音频播放问题:Expo AV库在应用后台时停止播放

使用Expo AV库开发应用时,前台音频播放正常,但进入后台后音频突然停止。已在音频初始化时设置staysActiveInBackground: true,并在app.json中配置了iOS后台音频项,但问题在iOS和Android设备上均存在,使用自定义AudioPlayer组件通过URI管理音频播放。

AudioPlayer组件代码

import React, { useState, useEffect } from "react";
import { StyleSheet, Text, View, TouchableOpacity } from "react-native";
import { Audio } from "expo-av";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import Slider from "@react-native-community/slider";

export default function AudioPlayer({ uri, stopAudio }) {
  const [audio, setAudio] = useState(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [position, setPosition] = useState(0);
  const [duration, setDuration] = useState(0);

  useEffect(() => {
    const loadAudio = async () => {
      if (uri) {
        const { sound, status } = await Audio.Sound.createAsync(
          { uri },
          { shouldPlay: true, staysActiveInBackground: true }
        );
        setAudio(sound);
        setDuration(status.durationMillis);
        setPosition(0);
        setIsPlaying(true);
      }
    };

    loadAudio();

    return () => {
      if (audio) {
        setIsPlaying(false);
      }
    };
  }, [uri]);

  useEffect(() => {
    if (audio) {
      const updatePosition = setInterval(async () => {
        const currentPosition = await audio.getStatusAsync();
        setPosition(currentPosition.positionMillis);
      }, 1000);

      return () => clearInterval(updatePosition);
    }
  }, [audio]);

  const setAudioMode = async () => {
    if (audio) {
      await audio.setAudioModeAsync({ staysActiveInBackground: true });
    }
  };

  useEffect(() => {
    setAudioMode();
  }, [audio]);

  useEffect(() => {
    if (audio) {
      if (stopAudio) {
        audio.stopAsync();
        setIsPlaying(false);
        setPosition(0);
      }
    }
  }, [stopAudio]);

  const handlePlayPause = async () => {
    if (audio) {
      if (isPlaying) {
        await audio.pauseAsync();
        setIsPlaying(false);
      } else {
        await audio.playAsync();
        setIsPlaying(true);
      }
    }
  };

  const handleSeek = async (value) => {
    if (audio) {
      await audio.setPositionAsync(value);
      setPosition(value);
    }
  };

  const handleStop = () => {
    if (audio) {
      audio.stopAsync();
      setIsPlaying(false);
      setPosition(0);
    }
  };

  return (
    <View style={styles.container}>
      <TouchableOpacity onPress={handlePlayPause} style={{ padding: 10 }}>
        {isPlaying ? (
          <Ionicons name="pause" size={24} color="black" />
        ) : (
          <Ionicons name="play" size={24} color="black" />
        )}
      </TouchableOpacity>

      <Slider
        style={styles.slider}
        minimumValue={0}
        maximumValue={duration}
        value={position}
        onSlidingComplete={handleSeek}
        disabled={!audio}
      />
      <Text>
        {Math.floor(position / 1000)}s / {Math.floor(duration / 1000)}s
      </Text>
      <TouchableOpacity onPress={handleStop} style={{ padding: 10 }}>
        <MaterialCommunityIcons name="stop" size={24} color="black" />
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    flexDirection: "row",
    flexWrap: "wrap",
  },
  slider: {
    width: "50%",
    marginTop: 20,
  },
});

app.json配置

{
  "expo": {
    "name": "newtourapp",
    "slug": "newtourapp",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "light",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true,
      "infoPlist": {
        "UIBackgroundModes": ["audio"]
      }
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.saqibansari.newtourapp"
    },
    "web": {
      "favicon": "./assets/favicon.png"
    }
  }
}

已尝试的解决方法

  • 在Audio.Sound.createAsync初始化时设置staysActiveInBackground: true
  • 在iOS的infoPlist中添加UIBackgroundModes: ["audio"]

解决方案

1. 全局配置音频模式

单个Sound实例的配置优先级低于全局音频模式,需在应用启动时全局设置后台播放权限:

useEffect(() => {
  const setupGlobalAudioMode = async () => {
    await Audio.setAudioModeAsync({
      staysActiveInBackground: true,
      allowsRecordingIOS: false,
      interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
      playsInSilentModeIOS: true,
      shouldDuckAndroid: false,
      interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
      playThroughEarpieceAndroid: false,
    });
  };

  setupGlobalAudioMode();

  // 组件卸载时重置模式
  return async () => {
    await Audio.setAudioModeAsync({
      staysActiveInBackground: false,
    });
  };
}, []);

将这段代码添加到AudioPlayer组件的根级useEffect中,确保全局模式先于Sound实例初始化。

2. 完善Android后台配置

Android 8.0+需要前台服务权限才能在后台播放音频,更新app.json的Android配置:

"android": {
  "adaptiveIcon": {
    "foregroundImage": "./assets/adaptive-icon.png",
    "backgroundColor": "#ffffff"
  },
  "package": "com.saqibansari.newtourapp",
  "permissions": ["android.permission.FOREGROUND_SERVICE"],
  "backgroundPermissions": ["audio"]
}

3. 修复组件清理逻辑

原组件的清理函数未正确卸载Sound实例,可能导致后台播放异常,修改为:

return () => {
  if (audio) {
    audio.unloadAsync();
    setAudio(null);
    setIsPlaying(false);
  }
};

4. 优化状态更新逻辑

避免在后台时意外触发音频停止,检查stopAudio的useEffect,确保仅在明确需要时停止:

useEffect(() => {
  if (audio && stopAudio) {
    audio.stopAsync();
    setIsPlaying(false);
    setPosition(0);
  }
}, [stopAudio, audio]);

5. 测试注意事项

  • iOS需使用真机测试,模拟器的后台播放限制较多
  • Android需在系统设置中开启应用的"后台运行权限"
  • 确保测试时设备未开启省电模式,省电模式会限制后台应用的音频播放

内容的提问来源于stack exchange,提问作者Saqib Ali

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 16:47:18