Flutter高仿微信-第34篇-单聊-小视频

news2025/8/4 4:41:28

Flutter高仿微信系列共59篇,从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。

 详情请查看

效果图:

详情请参考 Flutter高仿微信-第29篇-单聊 , 这里只是提取小视频的部分代码。

实现代码:

//打开相册权限
void _openAblumPermission() async {
  bool isPhotosGranted = await Permission.photos.isGranted;
  bool isPhotosDenied = await Permission.photos.isDenied;
  if(isPhotosGranted){
    _openAblum();
  } else {
    if(isPhotosDenied){
      _openAblum();
    } else {
      //跳转到设置页面提示
      _showPhotosConfirmationAlert(context);
    }
  }
}
//打开相册选择小视频
void _openAblum() {
  List<AssetEntity> selectedAssets = [];
  AssetPicker.pickAssets(
    context,
    pickerConfig: AssetPickerConfig(
      maxAssets: 1,
      selectedAssets: selectedAssets,
    ),
  ).then((imageList) {
    if(imageList == null){
      return;
    }
    imageList as List<AssetEntity>;
    for(int i = 0; i < imageList.length; i++){
      AssetEntity ae = imageList[i];
      ae.file.then((file) async {
        String resultFilePath = file?.path??"";
        _processVideoAndPicture(resultFilePath);
      });
    }
  });
}

//处理图片和小视频(相册、拍照)
void _processVideoAndPicture(String resultFilePath) async {

  if(resultFilePath == null || "" == resultFilePath){
    return;
  }

  String messageId = UUID.getUUID();

  if(CommonUtils.isVideo(resultFilePath)){
    /**
     * 小视频发送流程,因为小视频比较大,压缩时间比较长。发送的视频,先本地优先显示,查看播放小视频
     * 1、先复制一份小视频
     * 2、生成缩略图显示
     * 3、压缩小视频
     * 4、删除复制的小视频
     */
    //_testmp4(resultFilePath);

    //最大100M的视频, 不是1024*1024*500 , 使用1000*100*500
    int maxSize = 100000000;
    int fileSize = File(resultFilePath).lengthSync();
    if(fileSize > maxSize){
      CommonToast.show(context, "上传视频大小不能超过100M", duration: 3);
      return ;
    }

    //小视频生成缩略图大概5秒左右,先刷新站位图
    widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_VIDEO, resultFilePath, "", 0, messageId);

    String videoFormat = await getVideoFormat(resultFilePath);
    File srcFile = File(resultFilePath);
    String newVideoFileName = await FileUtils.getBaseFile("new_${DateUtil.getNowDateMs()}.mp4");
    srcFile.copySync(newVideoFileName);

    String thumbnailFileName = await FileUtils.getBaseFile("thum_${DateUtil.getNowDateMs()}.png");
    //生成缩略图
    await VideoThumbnail.thumbnailFile(video: resultFilePath, thumbnailPath: thumbnailFileName);
    //获取视频时间
    int second = await getVideoTime(resultFilePath);
    //int size = File(resultFilePath).lengthSync();
    //先刷新
    widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_VIDEO, resultFilePath, thumbnailFileName, second, messageId);
    //压缩完成再发送

    MediaInfo? mediaInfo = await CompressVideoUtils.compressVideo(newVideoFileName);
    String compressVideoPath = mediaInfo?.path??"";

    //int csecond = await getVideoTime(resultFilePath);
    //int csize = File(compressVideoPath).lengthSync();

    widget.sendMedialCallback(CommonUtils.CHAT_CONTENT_TYPE_VIDEO, compressVideoPath, second, messageId);
  }
}

//获取视频时间
Future<int> getVideoTime(String resultFilePath) async {
  int time = 0;
  final FlutterFFprobe _flutterFFprobe =  FlutterFFprobe();
  MediaInformation info = await _flutterFFprobe.getMediaInformation(resultFilePath);
  if (info.getStreams() != null) {
    String duration = info.getMediaProperties()?['duration'];
    String size = info.getMediaProperties()?['size'];
    double durationDouble = double.parse(duration);
    time = durationDouble.toInt();
    LogUtils.d("多媒体文件大小:${size}");
  }
  return time;
}

//刷新多媒体(图片、语音、小视频) (先刷新本地,然后小视频压缩完成再慢慢发送)
void _refreshMedia(int type, String mediaURL, String thumbnailFileName, {int mediaSecond=0, String messageId = "" }) async {

  bool isNetwork = await CommonNetwork.isNetwork();
  if(!isNetwork) {
    CommonUtils.showNetworkError(context);
    return;
  }

  bool deleteContacts = await isDeleteContacts(widget.account, widget.toChatId);
  if(deleteContacts){
    WnBaseDialog.showAddFriendsDialog(context, widget.toChatId);
    return;
  }

  String addTime = WnDateUtils.getCurrentTime();

  //先刷新本地聊天
  ChatBean chatBean = ChatBean(fromAccount: widget.account, toAccount: widget.toChatId,addTime:addTime,messageId: messageId,isRead: 1);


  chatBean.contentType = type;
  if(type == CommonUtils.CHAT_CONTENT_TYPE_VIDEO){
    //小视频会刷新本地2次。但messageId都是一样的
    chatBean.videoLocal = mediaURL;
    chatBean.imgPathLocal = thumbnailFileName;
    chatBean.second = mediaSecond;

    ChatBean? localChatBean = await ChatRepository.getInstance().findChatByMessageId(messageId);
    //状态变更,向聊天记录中插入新记录
    if(localChatBean == null){
      items.add(chatBean);
      ChatRepository.getInstance().insertChat(chatBean);
    } else {
      chatBean.id = localChatBean.id;
      ChatRepository.getInstance().updateChat(chatBean);
      //如果已经存在,先删除在添加
      for(int i = 0; i < items.length; i++){
        ChatBean item = items[i];
        if(item.messageId == messageId){
          items.remove(item);
          break;
        }
      }
      items.add(chatBean);
    }

    setState(() {

    });

  }


  //LogUtils.d("滚动到底部3");
  jumpToBottom(100);
}

//发送多媒体(图片、语音、小视频)
void _sendMedia(int type, String mediaURL, {int mediaSecond = 0, String messageId = ""}) async {

  bool isNetwork = await CommonNetwork.isNetwork();
  if(!isNetwork) {
    return;
  }

  bool deleteContacts = await isDeleteContacts(widget.account, widget.toChatId);
  if(deleteContacts){
    return;
  }

  //上传文件
  ChatBean serverChatBean;
  String message = "";
  ChatSendBean chatSendBean = ChatSendBean();
  chatSendBean.contentType = type;
  chatSendBean.messageId = messageId;
  chatSendBean.addTime = WnDateUtils.getCurrentTime();
  if(type == CommonUtils.CHAT_CONTENT_TYPE_VIDEO){
    //小视频
    serverChatBean = await UploadUtils.getInstance().uploadChatVideo(widget.account, widget.toChatId, mediaURL);
    message = "${type}${CommonUtils.CHAT_MESSAGE_SPILE}${serverChatBean.video}";
    chatSendBean.content = serverChatBean.video??"";
    chatSendBean.second = mediaSecond;
  } else {
    return ;
  }
  message = jsonEncode(chatSendBean);
  _sendMessage(message);
}

接收小视频:

String serverVideoPath = CommonUtils.BASE_URL_UPLOAD + content;
String localVideoPath = await FileUtils.getBaseFile("${DateUtil.getNowDateMs()}.mp4");
//先下载小视频
await DownloadUtils.getInstance().downloadFile(serverVideoPath, localVideoPath);
//生成缩略图
String? thumbnailFileName = await VideoThumbnail.thumbnailFile(video: localVideoPath);

chatBean.video = serverVideoPath;
chatBean.videoLocal = localVideoPath;
chatBean.imgPathLocal = thumbnailFileName??"";
chatBean.second = second;
//删除服务器文件
await HttpUtils.getInstance().deleteChatFile(content);

//通知栏提示
NotificationUtils.getInstance().showNotification(chatBean);
//插入本地数据库
ChatRepository.getInstance().insertChat(chatBean);
//EventBus刷新页面
eventBus.emit(chatBean);

显示小视频缩略图:

/**
 * Author : wangning
 * Email : maoning20080809@163.com
 * Date : 2022/11/16 0:42
 * Description : 单聊小视频缩略图, 已经存在缩略图图片,直接显示
 */
class CommonThumbnailWidget extends StatefulWidget {

  final String image;
  final VoidCallback onPressed;
  final EdgeInsetsGeometry padding;
  //视频多少秒
  final int second;
  //是否隐藏秒数
  bool isHideSecond;
  double width;
  double height;
  final bool isNetwork;

  CommonThumbnailWidget(
      {required this.image, required this.onPressed,required this.padding, required this.second,
        this.isHideSecond = false, this.width = 100, this.height = 200, this.isNetwork = false});

  @override
  State<CommonThumbnailWidget> createState() => _CommonThumbnailWidgetState();
}

class _CommonThumbnailWidgetState extends State<CommonThumbnailWidget> {

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return RawMaterialButton(

        materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
        padding: widget.padding,
        constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0),
        child:ClipRRect(
          borderRadius: BorderRadius.all(Radius.circular(1.0)),
          child: _getAssetFile(),
        ),
        onPressed: widget.onPressed
    );
  }

  //处理缩略图为空
  Widget _getAssetFile(){
    return Container(
      decoration: widget.image == "" ? boxDecoration(2):boxDecoration(0),
      child: Stack(
        alignment : AlignmentDirectional.center,
        children: [
          widget.image == "" ? SizedBox(width: widget.width, height: widget.height,): getFileWidget(widget.image),
          widget.image == "" ? const CircularProgressIndicator() : const Icon(Icons.play_arrow,color: Colors.white,size: 60.0,),
          Positioned(
            bottom: 10,
            right: 10,
            child: Offstage(
              offstage: widget.isHideSecond,
              child: Text("${CommonUtils.changeVideoTime(widget.second)}", style: TextStyle(color: Colors.white),),
            ),
          ),

        ],
      ),
    );
  }

  BoxDecoration boxDecoration(double width){
    return BoxDecoration(
      border: Border.all(
          width: width,
          color: Colors.grey
      ),
    );
  }

  Widget getFileWidget(String fileName){
    //文件存在
    if(File(fileName).existsSync()){
      return Image.file(File(widget.image),fit: BoxFit.cover,width: widget.width,height: widget.height,);
    } else if(widget.isNetwork){
      return CommonUtils.showBaseImage(CommonUtils.getReallyImage(widget.image), width: widget.width, height: widget.height);
    } else {
      return Image.asset(CommonUtils.getNetworkDefaultError(),fit: BoxFit.fitWidth,width: widget.width,height: widget.height,);
    }
  }

}

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

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

相关文章

Android App开发即时通信中通过SocketIO在客户端与服务端间传输文本和图片的讲解及实战(超详细 附源码)

需要源码和服务端代码请点赞关注收藏后评论区留下QQ~~~ 一、通过SocketIO传输文本消息 虽然HTTP协议能够满足多数常见的接口交互&#xff0c;但是他属于短连接&#xff0c;每次调用完就自动断开连接&#xff0c;并且HTTP协议区分了服务端和客户端&#xff0c;双方的通信过程是…

机器学习知识经验分享之三:基于卷积神经网络的经典目标检测算法

文章目录前言一、一阶段目标检测算法1.YOLO系列算法2.SSD检测算法3. RetinaNet检测算法二、两阶段目标检测算法1.Faster R-CNN检测算法2.Mask R-CNN检测算法3.Cascade R-CNN检测算法总结前言 本系列文章将对机器学习知识进行分享总结。便于大家从理论层面了解人工智能基础原理…

软件被人后台篡改了收款码属于入侵吗?

最近很多做平台的小伙伴&#xff0c;碰到了同样的问题&#xff0c;就是软件程序后台被恶意篡改收款二维码 这个问题出现在平台主身上无疑是雪上加霜&#xff0c;第一时间找到了小蚁君&#xff0c;分析了一下当时的情况&#xff0c;先安装了小蚁的入侵检测系统&#xff0c;显示…

计算机毕业设计之java+ssm协同办公系统

项目介绍 本公司文档协同办公管理系统采用SSM&#xff08;SpringSpringMVCMyBatis&#xff09;框架开发,主要包括系统用户管理模块、用户信息模块、文件信息管理、个人事务管理、资料信息管理、登录模块、和退出模块等多个模块. 本系统主要包含了等系统用户管理、用户信息管理…

webpack5 PWA解决Web App 项目网络离线情况没法访问情况

为什么 开发 Web App 项目&#xff0c;项目一旦处于网络离线情况&#xff0c;就没法访问了。 我们希望给项目提供离线体验。 是什么 渐进式网络应用程序(progressive web application - PWA)&#xff1a;是一种可以提供类似于 native app(原生应用程序) 体验的 Web App 的技术。…

Go语言中操作Redis

Redis介绍 Redis是一个开源的内存数据库&#xff0c;Redis提供了多种不同类型的数据结构&#xff0c;很多业务场景下的问题都可以很自然地映射到这些数据结构上。 除此之外&#xff0c;通过复制、持久化和客户端分片等特性&#xff0c;我们可以很方便地将Redis扩展成一个能够包…

Word控件Spire.Doc 【图像形状】教程(5) 如何在 C# 中将文本环绕在图像周围

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

frp篇---frp-notify + Gotify 实现 FRP 用户上线通知

frp-notify Gotify 实现 FRP 用户上线通知1. 安装frp_notify2. Gotify 配置3. frp-notify 配置启动 frp_notify开机自启动1. 安装frp_notify 一个专注于消息通知的 frp server manager plugin 实现&#xff0c;让你对进入 frps 的连接了如指掌&#xff0c;不再裸奔。 项目链…

SSM之Spring注解式缓存Redis

目录 Sprig整合Redis 导入相关pom依赖 添加对应的的配置文件 IEDA安装lombok插件 引入外部多文件 applicationContext.xml的整合配置文件 redis注解式缓存 Cacheable 测试类注解 Cacheable 的测试代码 CachePut CachePut测试代码 CacheEvict CacheEvict测试代码 Spr…

如何考察候选人 Vue 技术水平?

答对这些问题&#xff0c;检测你是否真正掌握了Vue 请说一下响应式数据的原理 默认 Vue 在初始化数据时&#xff0c;会给 data 中的属性使用 Object.defineProperty 重新定义所有属性&#xff0c;当页面到对应属性时&#xff0c;会进行依赖收集(收集当前组件中的 watcher)如果…

论文阅读【8】Conditional Random Fields: An Introduction

1.概述 1.1 论文相关 这篇论文是介绍一个经典模型&#xff0c;条件随机场&#xff08;CRF&#xff09;。在很多领域中都存在序列标注任务&#xff0c;例如生物信息识别&#xff0c;计算机语言学和语音识别任务&#xff0c;其中自然语言处理中的词性标注任何和命名实体识别任务…

JS 数据结构:链表

单链表 每个节点中只包含一个指针域的链表称为单链表。 头结点—其指针域指向表中第一个结点的指针&#xff08;头结点不是必须的&#xff0c;只是习惯上加上头结点&#xff0c;而头结点的数据域一般记录的是该链表的相关数据&#xff0c;如&#xff1a;链表长度&#xff09;…

Redis-Linux中安装Redis、命令操作Redis

目录 一、Redis简介 NoSQL与SQL的区别 二、Linux上安装redis 上传并解压redis.gz 进入 redis的解压目录&#xff0c;执行命令 make ​编辑 修改redis为守护进程 们测试一下能否远程连接RedisDesktopManager客户端 开放6379端口 授权&#xff0c;允许远程连接 三、redis命…

小程序上新(2022.10.13~11.14)

20221101 【官方公告】境外主体小程序补充信息存储地区通知20221103 小程序基础库 2.27.1 更新 新增 框架 新增 xr-fame 能力&#xff0c;kanata 更新 详情新增 组件 map 组件新增 bindrendersuccess 属性 详情 (官方文档还查不到这个)新增 API 新增 wx.getRendererUserAgen…

tep时隔8个月迎来重大全新升级

tep此次更新&#xff0c;旨在从“工具”升级为“框架”&#xff0c;为此做了大量的代码整洁工作&#xff0c;重新设计了部分功能&#xff0c;项目脚手架也焕然一新。 功能展示 conftest.py 脚手架生成的conftest.py只有一行代码&#xff1a; fixture自动加载等操作都隐藏到了te…

【学习笔记22】JavaScript数组的练习题

笔记首发 一、已知一个排序好的数组 将数字按照原有顺序插入到数组内 var arr [10, 20, 30, 40, 50];var n 11;// 1. 将n插入数组中arr.push(n);// 2. 冒泡排序for (var k 0; k < arr.length - 1; k) {for (var i 0; i < arr.length - 1 - k; i) {if (arr[i] > …

antd——使用a-tree组件实现 检索+自动展开+自定义增删改查功能——技能提升

之前写后台管理系统时&#xff0c;遇到一个下面的需求&#xff0c;下面是最终完成的效果图。 实现的功能有&#xff1a; 1. 下拉 选择不同的类型——就是一个普通的select组件&#xff0c;下面并不做介绍 2. 通过关键字可以进行tree树形结构的筛选&#xff0c;然后将筛选后的…

数据结构学习笔记——查找算法

目录前言一、查找的相关概念&#xff08;一&#xff09;内查找和外查找&#xff08;二&#xff09;静态查找和动态查找&#xff08;三&#xff09;平均查找长度二、线性查找&#xff08;一&#xff09;顺序查找1、查找思想2、算法分析3、有序表的顺序查找&#xff08;二&#x…

gitlab-runner 的安装使用(含 .gitlab-ci.yml 的简单使用)

简介 GitLab Runner 是一个开源项目&#xff0c;用于运行您的作业并将结果发送回 GitLab。它与 GitLab CI 一起使用&#xff0c;GitLab CI 是 GitLab 随附的开源持续集成服务&#xff0c;用于协调作业。 简单理解就是一个服务放在那儿&#xff0c;当你提交代码时&#xff0c;…

[附源码]java毕业设计在线课程网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…