flutter缓存网络视频到本地,可离线观看

news2025/5/18 17:30:01

记录一下解决问题的过程,希望自己以后可以参考看看,解决更多的问题。

需求:flutter 缓存网络视频文件,可离线观看。

解决:

1,flutter APP视频播放组件调整;

2,找到视频播放组件,传入url解析的地方;

 _meeduPlayerController.setDataSource(

       DataSource(
         //指定是网络类型的数据
         type: DataSourceType.network,
         //设置url参数
         source: widget.videoUrl != ""
             ? widget.videoUrl
             : "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",
         httpHeaders: {"Range": "bytes=0-1023"},
       ),


      autoplay: !background && widget.autoplay,
    );

3,那也就是无网路的时候播放本地已经缓存了的对应url的对应视频,先在没有缓存的时候,缓存该文件

        3.1,添加缓存(保存视频)功能依赖

                3.1.1,在视频播放依赖包中添加缓存依赖:flutter_cache_manager: ^3.4.1

                3.1.2,添加基于这个新依赖的功能代码:

import 'package:flutter_cache_manager/flutter_cache_manager.dart';

class CustomVideoCacheManager {
  static const key = 'customCacheKey';
  static final CacheManager instance = CacheManager(
    Config(
      key,
      maxNrOfCacheObjects: 50, // 最多缓存 50 个视频
      // maxTotalSize: 1024 * 1024 * 1024 * 2, // 最大缓存 2GB
      stalePeriod: Duration(days: 7), // 缓存保留时间
      repo: JsonCacheInfoRepository(databaseName: key),
      fileSystem: IOFileSystem(key),
      fileService: HttpFileService(),
    ),
  );
}

 暴露出来新加的dart类

                3.1.3,在video_widget组件中使用缓存工具缓存视频 

        3.2,在没有网的时候使用该缓存视频,改造步骤2中的播放方法:

_meeduPlayerController.setDataSource(
      //1网络时候的DataSource
      // DataSource(
      //   type: DataSourceType.network,
      //   source: widget.videoUrl != ""
      //       ? widget.videoUrl
      //       : "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",
      //   httpHeaders: {"Range": "bytes=0-1023"},
      // ),

      //2本地文件
      //错误写法:
      // DataSource(
      //   type: DataSourceType.file,
      //   // file: cacheFile,
      //   source:
      //       "/data/user/0/com.example.client/cache/customCacheKey/9dade030-3153-11f0-b119-93d21292c9e9.mp4",
      // ),
      //正确写法
      // DataSource(
      //   type: DataSourceType.file,
      //   // file: cacheFile,
      //   file: File(
      //       "/data/user/0/com.example.client/cache/customCacheKey/9dade030-3153-11f0-b119-93d21292c9e9.mp4"),
      // ),

      //所以根据条件判断用上边的任一个dataSource
      dataSource,

      autoplay: !background && widget.autoplay,
    );

        3.2.1,这个步骤中的插曲,就是使用本地文件一直报空,打印了_meeduPlayerController,和cacheFile都不为空,但是还是报空。

可能得问题有:

一,以为是异步写法awiat获得值,会产生后边的代码先于值计算出来,就运行了导致空

二,错误写法只是照搬了网络视频的写法,更换了一下type的参数,并没有多想,以为也是根据source来写;

解决问题一:

1看下await是怎么产生的。

        1.1,拿本地的缓存文件就有异步

        1.2,判断文件是否完成,是否可播也有await 

2如何避免;如果避免不了await,如何等值完全算完,不为空了再进行下一步的调用。

判断内容长度,是否下载完成,获取sp,判断是否可播都需要异步,就是不能直接拿到值。

Future<int?> getCachedContentLength(String url) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getInt('video_content_length_$url');
  }

  Future<void> cacheContentLength(String url, int length) async {
    final prefs = await SharedPreferences.getInstance();
    prefs.setInt('video_content_length_$url', length);
  }

  Future<bool> isVideoFileComplete(String url) async {
    // 获取之前缓存的原始大小
    final expectedLength = await getCachedContentLength(url);
    if (expectedLength == null) return false;

    // 获取本地缓存文件
    FileInfo? fileInfo = await DefaultCacheManager().getFileFromCache(url);
    final file = fileInfo?.file;
    if (file == null || !file.existsSync()) return false;

    final localLength = file.lengthSync();

    bool isSame = (localLength == expectedLength);
    print("video_widget 是否下载完成:$isSame");
    return isSame;
  }

  Future<bool> isVideoPlayable(String filePath) async {
    final controller = VideoPlayerController.file(File(filePath));
    try {
      await controller.initialize();
      await controller.dispose();
      print("video_widget 可以 正常播放");
      return true;
    } catch (e) {
      print("video_widget 不能 正常播放");
      return false;
    }
  }

所以用到这几个方法的设置setDataSource()方法也必定是异步的

// 单独封装异步判断逻辑
  Future<DataSource> _getDataSource(File? cachedFile, String url) async {
    if (cachedFile != null) {
      final exists = cachedFile.existsSync();
      final playable = await isVideoPlayable(cachedFile.path);
      final complete = await isVideoFileComplete(cachedFile.path);

      print("video_widget: cachedFile != null: ${cachedFile != null}");
      print("video_widget: existsSync: $exists");
      print("video_widget: isVideoPlayable: $playable");
      print("video_widget: isVideoFileComplete: $complete");

      if (exists && playable && complete) {
        print("video_widget:即将使用缓存视频");
        return DataSource(
          type: DataSourceType.file,
          source: cachedFile.path,
          httpHeaders: {"Range": "bytes=0-1023"},
        );
      }
    }

    // 如果没有命中缓存或缓存不完整,则走网络加载
    File? cacheFile;
    try {
      cacheFile = await CustomVideoCacheManager.instance.getSingleFile(url);
    } catch (e) {
      print("video_widget:网络文件获取失败: $e");
    }

    final networkSource = DataSource(
      type: DataSourceType.network,
      source: widget.videoUrl.isNotEmpty
          ? widget.videoUrl
          : "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",
      httpHeaders: {"Range": "bytes=0-1023"},
    );

    return cacheFile != null
        ? DataSource(
            type: DataSourceType.file,
            file: cacheFile,
          )
        : networkSource;
  }

使用这种方法,就不用在_meeduPlayerController设置参数的时候使用一个异步返回的dataSource了,代码如下,这样就可以使用一个看似同步的代码,完成了一个异步的操作。(并不会因为看起来像是同步的写法,就会发生dataSource的值还没回来的时候就执行了后边的代码,导致null产生。这个就是典型的支持异步操作的代码,不然就得像Java一样写回调了。)

// 封装异步获取 DataSource 的逻辑
dataSource = await _getDataSource(cachedFile, lastUrl);


_meeduPlayerController.setDataSource(
      
      //所以根据条件判断用上边的任一个dataSource
      dataSource,

      autoplay: !background && widget.autoplay,
    );

不用特意写then来完成这个异步操作,以下代码不推荐:

await _getDataSource(cachedFile, lastUrl).then((dataSource) {

      if (dataSource != null) {

        _meeduPlayerController.setDataSource(
          dataSource,
          autoplay: !background && widget.autoplay,
        );

      } else {
        print("video_widget:dataSource为空");
      }
    });

解决问题二:

1更换本地缓存文件的地址来写死dataSource参数,还是不行

2想到看下这个依赖包的说明文件是否支持本地文件播放,flutter_meedu_videoplayer example | Flutter package 看到是支持的,

3看依赖包的例子是怎么写的,没有具体写怎么播放本地视频

4看依赖包的源码是怎么使用

4.1,dataSource源码怎么使用的(只是设置参数,没有使用这个参数的逻辑)

4.2,那就找使用这个参数的源码:_meeduPlayerController,有怎么设置本地文件的DataSource方法,最终调整成正确的参数设置方式。

最后,以下是video_widget.dart的完整代码,仅供参考

import 'dart:async';
import 'dart:io';

import 'package:audio_session/audio_session.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_meedu_videoplayer/meedu_player.dart';
import 'package:game_lib/common/common_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wakelock_plus/wakelock_plus.dart';

class VideoWidget extends StatefulWidget {
  String videoUrl;
  Function? onVideoEnd;
  bool autoplay;
  Function(MeeduPlayerController)? onInit;
  Function(PlayerStatus)? onVideoStatusChanged;
  Function(Duration)? onVideoPositionChanged;
  bool closeFullscreenOnEnd;
  BoxFit fit;
  bool fullscreen;
  Function(bool)? onBackground;

  VideoWidget(
      {super.key,
      required this.videoUrl,
      this.onVideoEnd,
      this.onInit,
      this.autoplay = true,
      this.fullscreen = true,
      this.fit = BoxFit.contain,
      this.onVideoStatusChanged,
      this.closeFullscreenOnEnd = true,
      this.onVideoPositionChanged,
      this.onBackground});

  @override
  State<VideoWidget> createState() => _VideoWidgetState();
}

class _VideoWidgetState extends State<VideoWidget>
    with WidgetsBindingObserver, RouteAware {
  late final _meeduPlayerController = MeeduPlayerController(
      controlsStyle: ControlsStyle.primary,
      screenManager: const ScreenManager(orientations: [
        DeviceOrientation.landscapeLeft,
      ]),
      enabledButtons: EnabledButtons(
          videoFit: false, muteAndSound: false, fullscreen: widget.fullscreen),
      fits: [BoxFit.contain],
      initialFit: widget.fit);

  StreamSubscription? _playerEventSubs;
  int lastPosition = 0;
  String lastUrl = "";
  bool background = false; // 是否处于后台

  @override
  void initState() {
    WidgetsBinding.instance.addObserver(this);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      Future.delayed(const Duration(milliseconds: 500), () {
        _init();
      });
    });

    widget.onInit?.call(_meeduPlayerController);

    _meeduPlayerController.onPositionChanged.listen((event) {
      if (event.inSeconds != lastPosition) {
        lastPosition = event.inMilliseconds;
        widget.onVideoPositionChanged?.call(event);
        //print("onPositionChanged: $event ${event.inSeconds}");
      }
    });

    _playerEventSubs = _meeduPlayerController.onPlayerStatusChanged.listen(
      (PlayerStatus status) async {
        widget.onVideoStatusChanged?.call(status);
        print("onPlayerStatusChanged: $status");
        if (status == PlayerStatus.playing) {
          WakelockPlus.enable();
          Future.delayed(const Duration(milliseconds: 100), () {
            if (widget.fit == BoxFit.contain) {
              _meeduPlayerController.toggleVideoFit();
            }
          });
        } else {
          WakelockPlus.disable();
          final session = await AudioSession.instance;
          if (await session.setActive(false)) {
            print("AudioSession setActive abandon");
          }
        }

        if (status == PlayerStatus.completed) {
          if (widget.closeFullscreenOnEnd &&
              _meeduPlayerController.fullscreen.value &&
              Navigator.canPop(context)) {
//            Navigator.pop(context);
//            注释上面代码,播放完后不退出全屏
          }
          if (widget.onVideoEnd != null) {
            widget.onVideoEnd!();
          }
        }
      },
    );

    Timer? timer;

    _meeduPlayerController.onDataStatusChanged.listen((DataStatus status) {
      if (status == DataStatus.error) {
        setState(() {
          _meeduPlayerController.errorText = "";
        });
        print(
            "============= video widget onDataStatusChanged: $status videoUrl: ${widget.videoUrl}");
        if (widget.videoUrl.isNotEmpty) {
          timer?.cancel();
          timer = Timer(const Duration(milliseconds: 1), () {
            setSource();
          });
        }
      }
    });

    super.initState();
  }

  @override
  void dispose() {
    _playerEventSubs?.cancel();
    _meeduPlayerController.dispose();
    WidgetsBinding.instance.removeObserver(this);
    AppRouteObserver().routeObserver.unsubscribe(this);
    super.dispose();
  }

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    print("video widget didChangeAppLifecycleState: $state");
    final session = await AudioSession.instance;
    if (state == AppLifecycleState.resumed) {
      background = false;
      widget.onBackground?.call(background);
      _meeduPlayerController.play();
    } else if (state == AppLifecycleState.paused) {
      background = true;
      widget.onBackground?.call(background);
      _meeduPlayerController.pause();
    }
  }

  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!);
  }

  @override
  void didPushNext() {
    Future.delayed(const Duration(milliseconds: 500), () {
      if (!_meeduPlayerController.fullscreen.value) {
        _meeduPlayerController.pause();
      }
    });
  }

  _init() {
    print("autoplay: ${widget.autoplay}");
    setSource();
  }

  Future<void> setSource() async {
    if (widget.videoUrl == lastUrl) {
      return;
    }
    lastUrl = widget.videoUrl;

    File? cachedFile;
    DataSource? dataSource;

    try {
      print("video_widget:设置视频资源,lastUrl:$lastUrl");
      FileInfo? fileInfo =
          await CustomVideoCacheManager.instance.getFileFromCache(lastUrl);
      cachedFile = fileInfo?.file;
      print("video_widget:缓存文件地址${cachedFile?.path}");
    } catch (e) {
      print("video_widget:未找到缓存视频");
    }

    // 封装异步获取 DataSource 的逻辑
    dataSource = await _getDataSource(cachedFile, lastUrl);
    // await _getDataSource(cachedFile, lastUrl).then((dataSource) {
    //   print(
    //       "=====video_widget:_meeduPlayerController是否为空:${_meeduPlayerController == null}");
    //   print("=====video_widget:dataSource是否为空:${dataSource == null}");
    //   if (dataSource != null) {
    //     _meeduPlayerController.setDataSource(
    //       // DataSource(
    //       //   type: DataSourceType.network,
    //       //   source: widget.videoUrl != ""
    //       //       ? widget.videoUrl
    //       //       : "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",
    //       //   httpHeaders: {"Range": "bytes=0-1023"},
    //       // ),
    //
    //       dataSource,
    //
    //       autoplay: !background && widget.autoplay,
    //     );
    //   } else {
    //     print("video_widget:dataSource为空");
    //   }
    // });

    //清除缓存
    //await CustomVideoCacheManager.instance.emptyCache();

    _meeduPlayerController.setDataSource(
      // DataSource(
      //   type: DataSourceType.network,
      //   source: widget.videoUrl != ""
      //       ? widget.videoUrl
      //       : "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",
      //   httpHeaders: {"Range": "bytes=0-1023"},
      // ),

      // DataSource(
      //   type: DataSourceType.file,
      //   // file: cacheFile,
      //   file: File(
      //       "/data/user/0/com.example.client/cache/customCacheKey/9dade030-3153-11f0-b119-93d21292c9e9.mp4"),
      // ),

      dataSource,

      autoplay: !background && widget.autoplay,
    );
  }

  // 单独封装异步判断逻辑
  Future<DataSource> _getDataSource(File? cachedFile, String url) async {
    if (cachedFile != null) {
      final exists = cachedFile.existsSync();
      final playable = await isVideoPlayable(cachedFile.path);
      final complete = await isVideoFileComplete(cachedFile.path);

      print("video_widget: cachedFile != null: ${cachedFile != null}");
      print("video_widget: existsSync: $exists");
      print("video_widget: isVideoPlayable: $playable");
      print("video_widget: isVideoFileComplete: $complete");

      if (exists && playable && complete) {
        print("video_widget:即将使用缓存视频");
        return DataSource(
          type: DataSourceType.file,
          source: cachedFile.path,
          httpHeaders: {"Range": "bytes=0-1023"},
        );
      }
    }

    // 如果没有命中缓存或缓存不完整,则走网络加载
    File? cacheFile;
    try {
      cacheFile = await CustomVideoCacheManager.instance.getSingleFile(url);
    } catch (e) {
      print("video_widget:网络文件获取失败: $e");
    }

    final networkSource = DataSource(
      type: DataSourceType.network,
      source: widget.videoUrl.isNotEmpty
          ? widget.videoUrl
          : "https://movietrailers.apple.com/movies/paramount/the-spongebob-movie-sponge-on-the-run/the-spongebob-movie-sponge-on-the-run-big-game_h720p.mov",
      httpHeaders: {"Range": "bytes=0-1023"},
    );

    return cacheFile != null
        ? DataSource(
            type: DataSourceType.file,
            file: cacheFile,
          )
        : networkSource;
  }

  Future<int?> getCachedContentLength(String url) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getInt('video_content_length_$url');
  }

  Future<void> cacheContentLength(String url, int length) async {
    final prefs = await SharedPreferences.getInstance();
    prefs.setInt('video_content_length_$url', length);
  }

  Future<bool> isVideoFileComplete(String url) async {
    // 获取之前缓存的原始大小
    final expectedLength = await getCachedContentLength(url);
    if (expectedLength == null) return false;

    // 获取本地缓存文件
    FileInfo? fileInfo = await DefaultCacheManager().getFileFromCache(url);
    final file = fileInfo?.file;
    if (file == null || !file.existsSync()) return false;

    final localLength = file.lengthSync();

    bool isSame = (localLength == expectedLength);
    print("video_widget 是否下载完成:$isSame");
    return isSame;
  }

  Future<bool> isVideoPlayable(String filePath) async {
    final controller = VideoPlayerController.file(File(filePath));
    try {
      await controller.initialize();
      await controller.dispose();
      print("video_widget 可以 正常播放");
      return true;
    } catch (e) {
      print("video_widget 不能 正常播放");
      return false;
    }
  }

  @override
  Widget build(BuildContext context) {
    setSource();
    return AspectRatio(
      aspectRatio: 16 / 9,
      child: MeeduVideoPlayer(
        key: UniqueKey(),
        controller: _meeduPlayerController,
      ),
    );
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2378655.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kotlin 中 infix 关键字的原理和使用场景

在 Kotlin 中&#xff0c;使用 infix 关键字修饰的函数称为中缀函数&#xff0c;使用是可以省略 . 和 ()&#xff0c;允许以更自然&#xff08;类似自然语言&#xff09;的语法调用函数&#xff0c;这种特性可以使代码更具可读性。 1 infix 的原理 中缀函数必须满足以下条件&…

c++从入门到精通(五)--异常处理,命名空间,多继承与虚继承

异常处理 栈展开过程&#xff1a; 栈展开过程沿着嵌套函数的调用链不断查找&#xff0c;直到找到了与异常匹配的catch子句为止&#xff1b;也可能一直没找到匹配的catch&#xff0c;则退出主函数后查找过程终止。栈展开过程中的对象被自动销毁。 在栈展开的过程中&#xff0c…

gcc/g++常用参数

1.介绍 gcc用于编译c语言&#xff0c;g用于编译c 源代码生成可执行文件过程&#xff0c;预处理-编译-汇编-链接。https://zhuanlan.zhihu.com/p/476697014 2.常用参数说明 2.1编译过程控制 参数作用-oOutput&#xff0c;指定输出名字-cCompile&#xff0c;编译源文件生成对…

nginx配置之负载均衡

版权声明&#xff1a;原创作品&#xff0c;请勿转载&#xff01; 1.实验环境准备 准备3台linux服务器&#xff08;ubuntu和centos均可&#xff0c;本文使用centos7.9&#xff09;&#xff0c;两台web和一台负载均衡服务器&#xff0c;均安装nginx服务 主机名IP软件lb0110.0.0…

去年开发一款鸿蒙Next Os的window工具箱

持拖载多个鸿蒙应用 批量签名安装 运行 http://dl.lozn.top/lozn/HarmonySignAndFileManagerTool_2024-11-26.zip 同类型安卓工具箱以及其他软件下载地址汇总 http://dl.lozn.top/lozn/ 怎么个玩法呢&#xff0c;比如要启动某app, 拖载识别到包名 点启动他能主动读取包名 然后…

uniapp|实现获取手机摄像头权限,调用相机拍照实现人脸识别相似度对比,拍照保存至相册,多端兼容(APP/微信小程序)

基于uniapp以及微信小程序实现移动端人脸识别相似度对比,实现摄像头、相册权限获取、相机模块交互、第三方识别集成等功能,附完整代码。 目录 核心功能实现流程摄像头与相册权限申请权限拒绝后的引导策略摄像头调用拍照事件处理人脸识别集成图片预处理(Base64编码/压缩)调用…

JavaScript【7】BOM模型

1.概述&#xff1a; BOM&#xff08;Browser Object Model&#xff0c;浏览器对象模型&#xff09;是 JavaScript 中的一个重要概念&#xff0c;它提供了一系列对象来访问和操作浏览器的功能和信息。与 DOM&#xff08;Document Object Model&#xff09;主要关注文档结构不同&…

[强化学习的数学原理—赵世钰老师]学习笔记02-贝尔曼方程

本人为强化学习小白&#xff0c;为了在后续科研的过程中能够较好的结合强化学习来做相关研究&#xff0c;特意买了西湖大学赵世钰老师撰写的《强化学习数学原理》中文版这本书&#xff0c;并结合赵老师的讲解视频来学习和更深刻的理解强化学习相关概念&#xff0c;知识和算法技…

深入理解构造函数,析构函数

目录 1.引言 2.构造函数 1.概念 2.特性 3.析构函数 1.概念 2.特性 1.引言 如果一个类中什么都没有&#xff0c;叫作空类. class A {}; 那么我们这个类中真的是什么都没有吗?其实不是,如果我们类当中上面都不写.编译器会生成6个默认的成员函数。 默认成员函数:用户没有显…

Day 16

目录 1.JZ79 判断是不是平衡二叉树1.1 解析1.2 代码 2.DP10 最大子矩阵2.1 解析2.2 代码 1.JZ79 判断是不是平衡二叉树 JZ79 判断是不是平衡二叉树 dfs 1.1 解析 1.2 代码 /*** struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* TreeNode(in…

摄影构图小节

1、三分构图法 三分构图法即将画面横竖各分为三份&#xff0c;即九宫格形式。 将画面用两条竖线和两条横线分割&#xff0c;就如同是书写中文的【井】字。这样就可以得到4个交叉点&#xff0c;然后再将需要表现的重点放置在4个交叉点中的一个附近即可。 拍摄自然风光时&#xf…

DAY 28 类的定义和方法

知识点回顾&#xff1a; 类的定义pass占位语句类的初始化方法类的普通方法类的继承&#xff1a;属性的继承、方法的继承 比如def、class这些定义的关键词后&#xff0c;必须有一个有占据缩进位置的代码块。 还有下面这些依赖缩进的语句&#xff0c;都可以用pass语句来占位 x 1…

RAG数据处理:PDF/HTML

RAG而言用户输入的数据通常是各种各样文档&#xff0c;本文主要采用langchain实现PDF/HTML文档的处理方法 PDF文档解析 PDF文档很常见格式&#xff0c;但内部结构常常较复杂&#xff1a; 复杂的版式布局多样的元素&#xff08;段落、表格、公式、图片等&#xff09;文本流无…

机器学习 day04

文章目录 前言一、线性回归的基本概念二、损失函数三、最小二乘法 前言 通过今天的学习&#xff0c;我掌握了机器学习中的线性回归的相关基本概念&#xff0c;包括损失函数的概念&#xff0c;最小二乘法的理论与算法实现。 一、线性回归的基本概念 要理解什么是线性回归&…

蓝牙耳机什么牌子好?倍思值得冲不?

最近总被问“蓝牙耳机什么牌子好”&#xff0c;作为踩过无数坑的资深耳机党&#xff0c;必须安利刚入手的倍思M2s Pro主动降噪蓝牙耳机&#xff01;降噪、音质、颜值全都在线&#xff0c;性价比直接拉满。 -52dB降噪&#xff0c;通勤摸鱼神器 第一次开降噪就被惊到&#xff01…

机器学习-人与机器生数据的区分模型测试-数据处理 - 续

这里继续 机器学习-人与机器生数据的区分模型测试-数据处理1的内容 查看数据 中1的情况 #查看数据1的分布情况 one_ratio_list [] for col in data.columns:if col city or col target or col city2: # 跳过第一列continueelse:one_ratio data[col].mean() # 计算1值占…

ESP系列单片机选择指南:结合实际场景的最优选择方案

前言 在物联网(IoT)快速发展的今天&#xff0c;ESP系列单片机凭借其优异的无线连接能力和丰富的功能特性&#xff0c;已成为智能家居、智慧农业、工业自动化等领域的首选方案。本文将深入分析各款ESP芯片的特点&#xff0c;结合典型应用场景&#xff0c;帮助开发者做出最优选择…

特斯拉虚拟电厂:能源互联网时代的分布式革命

在双碳目标与能源转型的双重驱动下&#xff0c;特斯拉虚拟电厂&#xff08;Virtual Power Plant, VPP&#xff09;通过数字孪生技术与能源系统的深度融合&#xff0c;重构了传统电力系统的运行范式。本文从系统架构、工程实践、技术挑战三个维度&#xff0c;深度解析这一颠覆性…

【Linux笔记】nfs网络文件系统与autofs(nfsdata、autofs、autofs.conf、auto.master)

一、nfs概念 NFS&#xff08;Network File System&#xff0c;网络文件系统&#xff09; 是一种由 Sun Microsystems 于1984年开发的分布式文件系统协议&#xff0c;允许用户通过网络访问远程计算机上的文件&#xff0c;就像访问本地文件一样。它广泛应用于 Unix/Linux 系统&a…

博客打卡-求解流水线调度

题目如下&#xff1a; 有n个作业&#xff08;编号为1&#xff5e;n&#xff09;要在由两台机器M1和M2组成的流水线上完成加工。每个作业加工的顺序都是先在M1上加工&#xff0c;然后在M2上加工。M1和M2加工作业i所需的时间分别为ai和bi&#xff08;1≤i≤n&#xff09;。 流水…