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

Next.js中MediaStream卸载后无法停止及页面失活设备停止方案求助

问题解决:MediaStream清理失效及页面失活时停止设备

一、卸载时清理MediaStream失效的原因及修复

你的清理逻辑不生效,核心问题是useEffect闭包捕获的mediaStream是初始的undefined:

  • useState的更新是异步的,setMediaStream(stream)执行后,当前useEffect闭包中的mediaStream不会立刻更新
  • 你的useEffect依赖仅包含handleDevices,当mediaStream状态变化时,不会重新生成新的清理函数,导致卸载时执行的清理函数里mediaStream仍是初始值,无法触发track.stop()

修复方案:用useRef保存MediaStream引用

改用ref保存流引用,避免闭包带来的旧值问题,确保清理函数能拿到最新的MediaStream:

"use client"

import * as React from "react"
import { type Input, type Input as InputType } from "@/assets/playground/data/inputs"
import { generateUUID } from "@/lib/utils"

export default function PlaygroundEditor() {
  const [inputs, setInputs] = React.useState<InputType[]>([])
  const [selectedInput, setSelectedInput] = React.useState<Input>()
  // 用ref保存stream,避免闭包捕获旧值
  const mediaStreamRef = React.useRef<MediaStream | null>(null)

  const handleDevices = React.useCallback(
    async (mediaDevices) => {
      setInputs(
        mediaDevices
          .filter((device) => device.kind === "videoinput")
          .map((device) => ({
            id: generateUUID(),
            name: device.label,
            type: "device",
            selected: device,
          }))
      )
    },
    [] // useState的setter是稳定引用,无需加入依赖
  )

  // 封装通用的停止流函数
  const stopMediaStream = () => {
    if (mediaStreamRef.current) {
      mediaStreamRef.current.getVideoTracks().forEach(track => {
        track.stop()
        mediaStreamRef.current = null
      })
    }
  }

  React.useEffect(() => {
    const requestDevice = async () => {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: true })
        mediaStreamRef.current = stream
      } catch (err) {
        console.error(err)
      }
    }

    requestDevice().then(() => {
      navigator.mediaDevices.enumerateDevices().then(handleDevices)
    })

    // 卸载时执行清理
    return stopMediaStream
  }, [handleDevices])

  // 其他状态逻辑
  const [timer, setTimer] = React.useState("00:00:00")
  // 注意:原代码中Model、models未定义,需自行补充相关类型与数据
  // const [selectedModel, setSelectedModel] = React.useState<Model>(models[0])

  return <div>hi</div>
}

二、页面失活时停止媒体设备

监听浏览器visibilitychange事件,当页面切换到隐藏状态时停止流,恢复可见时可按需重新请求设备:

React.useEffect(() => {
  const handleVisibilityChange = () => {
    if (document.hidden) {
      // 页面隐藏时停止摄像头
      stopMediaStream()
    } else {
      // 页面恢复可见时重新请求设备(可选,根据业务需求调整)
      const requestDeviceAgain = async () => {
        try {
          const stream = await navigator.mediaDevices.getUserMedia({ video: true })
          mediaStreamRef.current = stream
        } catch (err) {
          console.error(err)
        }
      }
      requestDeviceAgain()
    }
  }

  document.addEventListener("visibilitychange", handleVisibilityChange)
  return () => {
    document.removeEventListener("visibilitychange", handleVisibilityChange)
  }
}, [])

补充说明

  • 若不需要页面恢复时自动开启摄像头,可直接删除else分支的逻辑
  • 停止track后,浏览器摄像头指示灯会熄灭,符合用户对隐私的预期
  • 原代码中handleDevices的依赖可简化为[],因为setInputs是React提供的稳定函数,无需加入依赖数组

内容的提问来源于stack exchange,提问作者James Wong

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 01:57:21