7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

アップロードする動画の一部を画像として切り出す(Nuxt.js/Vue.js/Vuetify)

7
Last updated at Posted at 2019-11-07

概要

以前の記事の応用です。
https://qiita.com/Yoshihiro-Hirose/items/82b9dccdf270551db887
動画ファイルを選択してプレビュー表示して、その一部をマウスでクリックして画像に切り出します。

動作サンプル

CodePenで動作するサンプルを用意しました。
狭いので右上のロゴから別ウィンドウを推奨します。
適当な動画を選択して、マウスでクリックしてみてください。
※CodePen 上で動かすと座標が多少ずれます

See the Pen SelectVideoPointClip by Yoshihiro Hirose (@yoshihiro-hirose) on CodePen.

処理の流れ

  1. マウスカーソルの画像を用意
  2. 動画ファイル選択時に表示する video 要素にクリックイベントを登録
  3. クリック時の座標を計算して canvas に書き出す

マウスカーソルの画像を用意

クリップする感を出すためにカーソル用の画像を用意します。
枠があって中が透過されていれば OK。
clip_cursol.png

static ディレクトリに画像を配置して、vue ファイルにもスタイルを定義して video 要素から参照させます。

<style lang="sass" scoped>
.clipable
  cursor: url('/clip_cursol.png'), crosshair
</style>

動画ファイル選択時に表示する video 要素にクリックイベントを登録

@click で関数を指定してビデオ内のクリックを捕捉します。
再生ボタンや早送りなどのコントロールはクリックイベントの対象外です。

          <video
            :
            :
            :
            @click="clip"
          >

クリック時の座標を計算して canvas に書き出す

画像を書き出す canvas を用意しておきます。

          <canvas
            id="c"
            :width="matching_image_side"
            :height="matching_image_side"
          ></canvas>

ビデオクリック時の関数は以下のようになりました。

    clip(event) {
      const video = document.getElementById('upload-video')
      const canvas = document.getElementById('c')

      // 実際のビデオサイズ
      const orgVideoWidth = video.videoWidth
      const orgVideoHeight = video.videoHeight

      // ブラウザ上の表示サイズ
      const eleVideoWidth = video.width
      const eleVideoHeight = video.height

      // 割合を計算して処理していく
      const widthRatio = orgVideoWidth / eleVideoWidth
      const heightRatio = orgVideoHeight / eleVideoHeight

      // カーソルの枠分ピクセルを調整
      const pointX = event.offsetX + 3
      const pointY = event.offsetY + 3

      const srcX = pointX * widthRatio
      const srcY = pointY * heightRatio

      canvas
        .getContext('2d')
        .drawImage(
          video,
          srcX,
          srcY,
          this.matching_image_side,
          this.matching_image_side,
          0,
          0,
          this.matching_image_side,
          this.matching_image_side
        )
    },

以下工夫ポイント

  • クリックした座標からマウスカーソルの枠分を考慮して調整
  • drawImage で video から書き出す際、実際の動画のサイズが基準になっている。
    • ブラウザ上で表示した場合に比べて差があるので、割合を計算して調整している。

おまけ

canvas に書き出した画像を API からアップロードしたい場合、Blob を生成して multipart で送れます。
詳細は全体のコードを確認してください。

ファイル全体

sample.vue
<template>
  <v-row justify="center">
    <v-col sm="12" md="11" lg="9" xl="6">
      <v-sheet class="pa-3">
        <h1>動画のマウスクリックによる一部切り出し</h1>
        <v-form ref="form">
          <video
            v-if="uploadVideoUrl"
            id="upload-video"
            class="clipable"
            controls
            width="640"
            height="360"
            @click="clip"
          >
            <source :src="uploadVideoUrl" />
            このブラウザではビデオ表示がサポートされていません
          </video>
          <v-file-input
            v-model="work_video"
            accept="video/*"
            show-size
            label="適当な動画ファイルを選択してください"
            prepend-icon="mdi-video"
            @change="onVideoPicked"
          ></v-file-input>
          <canvas
            id="c"
            :width="matching_image_side"
            :height="matching_image_side"
          ></canvas>
          <span v-if="uploadVideoUrl"
            >※動画をクリックして選択箇所を画像に切り出します</span
          >
        </v-form>
        <v-row align="center" justify="center" class="mt-12 mb-4 px-5">
          <v-btn outlined color="iconcolor" rounded block @click="post"
            >サーバにPOSTする予定ボタン</v-btn
          >
        </v-row>
      </v-sheet>
    </v-col>
  </v-row>
</template>

<script>
export default {
  data() {
    return {
      work_video: null,
      matching_image_side: '28',
      matching_image: null,
      uploadVideoUrl: ''
    }
  },
  methods: {
    onVideoPicked(file) {
      if (file !== undefined && file !== null) {
        if (file.name.lastIndexOf('.') <= 0) {
          return
        }
        const fr = new FileReader()
        fr.readAsDataURL(file)
        fr.addEventListener('load', () => {
          this.uploadVideoUrl = fr.result
        })
      } else {
        this.uploadVideoUrl = ''
      }
    },
    clip(event) {
      const video = document.getElementById('upload-video')
      const canvas = document.getElementById('c')

      // 実際のビデオサイズ
      const orgVideoWidth = video.videoWidth
      const orgVideoHeight = video.videoHeight

      // ブラウザ上の表示サイズ
      const eleVideoWidth = video.width
      const eleVideoHeight = video.height

      // 割合を計算して処理していく
      const widthRatio = orgVideoWidth / eleVideoWidth
      const heightRatio = orgVideoHeight / eleVideoHeight

      // カーソルの枠分ピクセルを調整
      const pointX = event.offsetX + 3
      const pointY = event.offsetY + 3

      const srcX = pointX * widthRatio
      const srcY = pointY * heightRatio

      canvas
        .getContext('2d')
        .drawImage(
          video,
          srcX,
          srcY,
          this.matching_image_side,
          this.matching_image_side,
          0,
          0,
          this.matching_image_side,
          this.matching_image_side
        )
    },
    async post() {
      const blobPromise = new Promise(function(resolve, reject) {
        const canvas = document.getElementById('c')
        canvas.toBlob(
          function(blob) {
            resolve(blob)
          },
          'image/jpeg',
          1.0
        )
      })

      const formData = new FormData()
      formData.append('image', await blobPromise, 'test.jpg')

      // API で切り抜いた画像をアップロードできる
      // await this.$axios.post('/api/test', formData, {
      //   headers: {
      //     'Content-Type': 'multipart/form-data'
      //   }
      // })
    }
  }
}
</script>

<style lang="sass" scoped>
.clipable
  cursor: url('/clip_cursol.png'), crosshair
</style>

最後に

指摘事項や質問ありましたらお気軽にコメントください!

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?