专栏首页smackgg最近很火的 倒放挑战 - ReverseVoice (微信小程序版 前后端源码) Ts Node Taro
原创

最近很火的 倒放挑战 - ReverseVoice (微信小程序版 前后端源码) Ts Node Taro

项目地址: https://github.com/smackgg/reversevoice

整个项目其实很简单,从本人在抖音和 B 站看到火起来到最终小程序上线也就几天的下班时间就搞定了,11月16日上线至今用户量还是蛮多的(主要当时做的快此类 app 比较少😂),现在已经出现了大量的更简约更好的倒放挑战 app,本项目开源仅供大家学习~

拥抱 TypeScript ~

顺便小声吐槽一下 Taro 对 Ts 的支持还是不够啊,希望大家多去给 Taro 提 dts 的 PR ~

体验

小程序二维码

下载 (1).png

挑战分享海报 (这个海报暂时有问题,修复代码因为资质问题还没有提交审核)

1.png

功能介绍/实现原理

  1. 功能及实现原理简述
  2. 小程序端用户录音并保存本地
  3. 录音后将录音文件上传至后端进行倒放处理,并返回处理后的音频 url
  4. 小程序端下载 url 文件,提示用户反转成功,将数据做本地 map
  5. 用户点击分享,生成分享链接,并将该分享正放、倒放视频均传至后端保存至七牛云
  6. 同时新建分享 room 保存用户信息,返回 roomId
  7. 用户分享(海报分享 canvas 动态生成分享码海报)
  8. 其它用户参加挑战,存储原理同 4,只是增加将挑战者信息了存入 room 的逻辑
  9. 音频倒放 使用 ffmpeg 进行音频倒放,核心代码:

// 详见 ./server/src/controllers/file.ts => function reverseVoice
import ffmpegPath from '@ffmpeg-installer/ffmpeg'
import ffprobePath from '@ffprobe-installer/ffprobe'
import ffmpeg from 'fluent-ffmpeg'
ffmpeg.setFfprobePath(ffprobePath.path)
ffmpeg.setFfmpegPath(ffmpegPath.path)

ffmpeg(filepath)
    .format('mp4')
    // 反转
    .outputOptions([
      '-vf reverse',
      '-af areverse',
      '-preset',
      'superfast',
      '-y',
    ])
    .on('progress', (progress) => {
      // send upload progress
      console.log('upload-file-progress', progress.percent)
    })
    .on('error', (err) => {
      console.log(`Ffmpeg has been killed${err.message}`)
    })
    .toFormat('mp3')
    // 保存
    .save(publicPath + saveFilePath)
    .on('end', () => {
      // 获取音频信息(时长等)
      ffmpeg.ffprobe(publicPath + saveFilePath, (err, metadata) => {
        console.log(metadata.format.duration)
      })
    })

录音

// 画图
const draw = async () => {
  // 绘制之前 loading
  Taro.showLoading({
    title: '海报生成中...',
    mask: true,
  })
  // 获取图片信息
  const [productImgInfo, qrcodeImgInfo] = await Promise.all([
    this.getImageInfo(sharePoster), // 获取主图
    this.getQrImgInfo(), // 获取二维码图片
  ])

  // product image 宽高
  const pW = CANVAS_WIDTH
  const pH = (pW / productImgInfo.width) * productImgInfo.height

  // canvas 高度
  let canvasHeight = pH

  const ctx = Taro.createCanvasContext('canvas', null)

  ctx.fillStyle = '#fff'
  ctx.fillRect(0, 0, CANVAS_WIDTH, canvasHeight)

  // 绘制背景图片
  ctx.drawImage(sharePoster, 0, 0, pW, pH)

  // 绘制二维码 (因为有角度,需要旋转画布,再旋转回来)
  ctx.rotate(-Math.PI / 32)
  ctx.translate(-25 * ratio, 10 * ratio)
  ctx.drawImage(qrcodeImgInfo.path, QR_LEFT, QR_TOP, QR_WIDTH, QR_WIDTH)
  ctx.rotate(Math.PI / 32)
  this.setState({
    canvasStyle: {
      ...this.state.canvasStyle,
      height: canvasHeight,
    },
  })
  ctx.stroke()
  setTimeout(() => {
    Taro.hideLoading()
    ctx.draw()
  }, 500)
}
// 微信小程序每个页面几乎都需要配置分享的参数,并且需要动态更改分享参数
// 所以抽离 HOC 组件,方便页面使用
import { ComponentClass } from 'react'

import Taro from '@tarojs/taro'
import { connect } from '@tarojs/redux';
import defaultShareImg from '@/assets/images/share.png'

type Options = {
  title?: string
  imageUrl?: string
  path?: string
}

const defalutOptions: Options = {
  title: '你能听懂我说啥么?最近很火的反转录音来啦~',
  imageUrl: defaultShareImg,
  path: 'pages/index/index',
}

function withShare() {
  return function demoComponent(Component: ComponentClass) {
    @connect(({ user }) => ({
      userInfo: user.userInfo
    }))
    class WithShare extends Component {
      $shareOptions?: Options
      async componentWillMount() {
        Taro.showShareMenu({
          withShareTicket: true,
        })

        if (super.componentWillMount) {
          super.componentWillMount()
        }
      }

      // 点击分享的那一刻会进行调用
      onShareAppMessage() {
        // const sharePath = `${path}&shareFromUser=${userInfo.shareId}`
        let options = defalutOptions
        if (this.$shareOptions) {
          options = {
            ...defalutOptions,
            ...this.$shareOptions,
          }
        }
        return options
      }

      render() {
        return super.render()
      }
    }

    return WithShare
  }
}

export default withShare

使用

@withShare()
class Room extends Component {
  /**
 * 指定config的类型声明为: Taro.Config
 *
 * 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型
 * 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string
 * 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型
 */
  config: Config = {
    navigationBarTitleText: '首页',
  }

  $shareOptions = {
    title: '倒放挑战!你能听懂我倒立洗头~',
    path: 'pages/index/index',
    imageUrl: '',
  }

  /**
    ....
  */
}

项目运行 - 后端

准备

需要提前安装:

开始

  • 克隆项目并进入后端目录cd servernpm install# create the db directory sudo mkdir -p /data/db # give the db correct read/write permissions sudo chmod 777 /data/db # starting from macOS 10.15 even the admin cannot create directory at root # so lets create the db diretory under the home directory. mkdir -p ~/data/db # user account has automatically read and write permissions for ~/data/db.mongod # on macOS 10.15 or above the db directory is under home directory mongod --dbpath ~/data/dbnpm run build npm start
  • 安装依赖
  • 设置 mongoDB
  • 启动 mongoDB (Start your mongoDB server (you'll probably want another command prompt)
  • 打包并运行项目

项目运行 - 小程序端

准备

需要提前安装:

开始

  • 克隆项目并进入小程序目录cd wechatappnpm install
  • 安装依赖
  • 新建 .env 文件在 wechatapp/src/utils 目录下克隆 env.example.ts 文件至同目录命名为 .env.ts 文件 此文件两个参数分别代表本地开发和线上部署的请求地址
  • 运行项目npm run dev:weapp // development mode 或者 npm run build:weapp // production mode
  • 微信开发者工具选择导入项目,并选择 wechatapp/dist 目录 若本地开发,需要在开发者工具中设置开启“不校验合法域名“

License

MIT

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • .NET手撸绘制TypeScript类图——上篇

    近年来随着交互界面的精细化, TypeScript越来越流行,前端的设计也越来复杂,而 类图正是用简单的箭头和方块,反映对象与对象之间关系/依赖的好方式。许多工...

    心莱科技雪雁
  • Typescript 严格模式有多严格?

    "use strict"指令在JavaScript 1.8.5 (ECMAScript5)中新增。

    用户1462769
  • TypeScript真香系列——接口篇

    这对于我们之前在写 JavaScript 的时候,再正常不过了,但是如果这个 getUserInfo 在多人开发过程中,如果它是个公共函数,多个开发者都会调用,...

    coder_koala
  • 干货 | Taro 开发微信小程序入门与实战

    如果你是 Vue 开发者,但又不想过多的折腾,继续沿用 Vue 的开发方式来开发小程序,那么用 mpvue 将会是你的最明智的选择。同时也支持一些第三方 UI ...

    极乐君
  • Python快速安装库的靠谱办法

    作者:Allen

    叫我龙总
  • 发布TS类型文件到npm

    TS类型文件的包名通常以@types开头,使用npm publish发布以@types开头的包时需要使用付费账号。

    雪飞鸿
  • TypeScript 黑魔法之编译选项设置

    在TypeScript中,有些地方对“开箱即用”进行了限制,例如当使用一个未被声明过的变量时(当然,你可以为外部系统使用声明文件)。

    用户1682855
  • Vue3 深度解析

    距离尤雨溪首次公开 Vue3 (vue-next)源码有一个多月了。青笔观察到,刚发布国庆期间,出现不少解读 Vue3 源码的文章。当然不少有追风蹭热之嫌,文章...

    我是一条小青蛇
  • 面向接口编程

    我们经常说一个库或者模块对外提供了某某API。通过主动暴露的接口来通信,可以隐藏软件系统内部的工作细节。这也是我们最熟悉的第一种接口含义。

    一粒小麦
  • Node.js服务端开发教程 (五):依赖注入进阶篇

    在前一篇文章《依赖注入基础篇》中,我们了解了依赖注入和控制反转的基本概念,大致知道它是怎么一回事。并通过简单的例子,学习到了在NestJS框架下如何使用依赖注入...

    一斤代码

扫码关注云+社区

领取腾讯云代金券