【自由研究】セミの羽化をAmazon Kinesis Video Streamsへリアルタイムに配信してみた。

【自由研究】セミの羽化をAmazon Kinesis Video Streamsへリアルタイムに配信してみた。

Clock Icon2025.07.18

はじめに

皆様こんにちは、あかいけです。
最近は猛暑の日が続いておりますが、いかがお過ごしでしょうか。

夏といえば夏休み。
夏休みといえば自由研究。
自由研究といえばセミの羽化観察。

というわけで今回は自由研究として、
セミの羽化を Amazon Kinesis Video Streams へリアルタイムに配信してみました。

モチベーション

しかしわざわざ KVS で記録する必要はあるのでしょうか?
ただセミの羽化を観察するだけならその場で観察すればいいだけではないでしょうか。
自分の目で観察対象を直接目視して、得られた発見に胸を躍らせる、それが自由研究の醍醐味でしょう。

しかしいくつかの懸念によって私は、録画して、
なおかつ KVS へ配信する必要性に気づいてしまったのです。

懸念 1. セミの羽化を待たずして寝てしまう。

はい、これは想像しやすいかと思います。
最近の子供にとって夜更かしは当たり前かもしれませんが、正直当時の私はセミの羽化を見届ける前に確実に寝てしまっていました。

なのでおのずと動画として記録しておく選択肢がまず思いつきます。
スマホなりパソコンなり、または家の片隅に転がっているビデオカメラを引っ張り出してくるわけですね。

素晴らしい!これで問題は何もないでしょう。
しかしここで次の懸念が出てきます。

懸念 2. 羽化したセミのオシッコによって機材が破壊されてしまう。

これも想像しやすいでしょう。
そうです、セミといえばオシッコ。

羽化直後のセミのオシッコ率がどれぐらいかは知りませんが、
無慈悲なオシッコによってビデオカメラが破壊される様子が容易に想像できます。
そして貴重なセミの羽化シーンは忘却の彼方へと霧散していくのです…。

ならば録画データを遠隔地へリアルタイムでバックアップしておくのがベストと考えられるでしょう。
そして私は AWS に力を入れている会社に所属しています、もうこれは KVS を使わない理由はないでしょう。

構成図

今回の構成は以下の通りです。
シンプルにローカル端末で録画したデータをそのまま KVS へ配信しています。
また配信したデータは KVS に保存されているので、そのほかの保存先は用意していません。

image.drawio

なお GStreamer が二つある理由ですが、これは今回物理カメラを使いたいためです。

まず Docker 上の GStreamer は物理カメラを認識できず、ネットワークカメラを利用する必要があります。
ただ今回は Mac に USB 接続した Web カメラを利用したいので、
Mac 上で GStreamer を起動して、そのデータを Docker 上の GStreamer へ連携、
そして最終的に KVS へ配信しています。

事前準備

セミの幼虫

1000001326

今回のゲストです。
彼 (または彼女) なくしてこのブログは成立しません。
今回はオフィス近くの日比谷公園で探してみました。

最初こそ「こんなコンクリートジャングルに果たしてセミがいるのだろうか…。」、と不安になりましたが、意外と見つけられました。
ただ開始 5 分ぐらいで発見できたので、かなりラッキーだったのかもしれません。

Web カメラ

1000001325

セミの幼虫を録画するためのカメラです。
別に Mac 内蔵のカメラでもいいんですが、セミの幼虫をカーテンにデプロイする都合上、Web カメラの方が扱いやすいのでこちらを利用します。

Mac

KVS へ映像を配信するためのプロデューサーライブラリ (GStreamer) を動かします。
今回は Docker を使うので、事前に Docker のインストールを済ませておきます。

AWS 側 リソース作成

まずは AWS 上のリソースを作成します。
作成するリソースは以下だけです。

  • 配信先の KVS ビデオストリーム
  • プロデューサー用 IAM ユーザー/IAM ポリシー

あと Amazon Kinesis Video Streams 上のデータ保存期間はセミにちなんで 7 日にしてみました。

main.tf
terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 6.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# Kinesis Video Stream
resource "aws_kinesis_video_stream" "mac_camera_stream" {
  name                    = var.stream_name
  data_retention_in_hours = var.data_retention_hours
  media_type              = "video/h264"

  tags = {
    Name        = var.stream_name
    Environment = var.environment
  }
}

# IAM Role for Kinesis Video Streams
resource "aws_iam_role" "kinesis_video_role" {
  name = "${var.stream_name}-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "kinesisvideo.amazonaws.com"
        }
      },
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
      }
    ]
  })

  tags = {
    Name        = "${var.stream_name}-role"
    Environment = var.environment
  }
}

# IAM Policy for Kinesis Video Streams
resource "aws_iam_policy" "kinesis_video_policy" {
  name        = "${var.stream_name}-policy"
  description = "Policy for Kinesis Video Streams access"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "kinesisvideo:CreateStream",
          "kinesisvideo:DescribeStream",
          "kinesisvideo:GetDataEndpoint",
          "kinesisvideo:PutMedia",
          "kinesisvideo:GetMedia",
          "kinesisvideo:ListStreams",
          "kinesisvideo:UpdateStream",
          "kinesisvideo:GetStreamSession",
          "kinesisvideo:StartStreamEncryption",
          "kinesisvideo:StopStreamEncryption"
        ]
        Resource = [
          aws_kinesis_video_stream.mac_camera_stream.arn,
          "${aws_kinesis_video_stream.mac_camera_stream.arn}/*",
          "arn:aws:kinesisvideo:${var.aws_region}:${data.aws_caller_identity.current.account_id}:stream/*"
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "kinesisvideo:ListStreams"
        ]
        Resource = "*"
      }
    ]
  })
}

# Attach Policy to Role
resource "aws_iam_role_policy_attachment" "kinesis_video_policy_attachment" {
  role       = aws_iam_role.kinesis_video_role.name
  policy_arn = aws_iam_policy.kinesis_video_policy.arn
}

# IAM User for application access (optional)
resource "aws_iam_user" "kinesis_video_user" {
  name = "${var.stream_name}-user"

  tags = {
    Name        = "${var.stream_name}-user"
    Environment = var.environment
  }
}

# Attach Policy to User
resource "aws_iam_user_policy_attachment" "kinesis_video_user_policy_attachment" {
  user       = aws_iam_user.kinesis_video_user.name
  policy_arn = aws_iam_policy.kinesis_video_policy.arn
}

# Access Key for the user (optional, for programmatic access)
resource "aws_iam_access_key" "kinesis_video_access_key" {
  user = aws_iam_user.kinesis_video_user.name
}

# Data source for current AWS account
data "aws_caller_identity" "current" {}
variables.tf
variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "ap-northeast-1"
}

variable "stream_name" {
  description = "Name of the Kinesis Video Stream"
  type        = string
  default     = "dockerStream"
}

variable "data_retention_hours" {
  description = "Data retention period in hours"
  type        = number
  default     = 168  # 7 days (7 * 24 hours)
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}
outputs.tf
output "kinesis_video_stream_name" {
  description = "Name of the Kinesis Video Stream"
  value       = aws_kinesis_video_stream.mac_camera_stream.name
}

output "kinesis_video_stream_arn" {
  description = "ARN of the Kinesis Video Stream"
  value       = aws_kinesis_video_stream.mac_camera_stream.arn
}

output "kinesis_video_stream_creation_time" {
  description = "Creation time of the Kinesis Video Stream"
  value       = aws_kinesis_video_stream.mac_camera_stream.creation_time
}

output "iam_role_arn" {
  description = "ARN of the IAM role for Kinesis Video Streams"
  value       = aws_iam_role.kinesis_video_role.arn
}

output "iam_user_name" {
  description = "Name of the IAM user for programmatic access"
  value       = aws_iam_user.kinesis_video_user.name
}

output "access_key_id" {
  description = "Access Key ID for the IAM user"
  value       = aws_iam_access_key.kinesis_video_access_key.id
}

output "secret_access_key" {
  description = "Secret Access Key for the IAM user"
  value       = aws_iam_access_key.kinesis_video_access_key.secret
  sensitive   = true
}

以下のコマンドでデプロイします。

terraform init;
terraform apply;

IAM ユーザーのクレデンシャル情報は後で使うので、以下のコマンドで確認しておきます。

terraform output --json

ローカル側 リソース作成

次はローカル側のリソース作成です。

ほぼほぼ以下の記事の手順を参考にしています、
めちゃめちゃ助かりました、ありがとうございます!

https://dev.classmethod.jp/articles/kinesis-video-streams-docker-with-mac-cam/

Docker

まずは AWS 公式が提供している ECR リポジトリにログインして、Docker Image を Pull します。

aws ecr get-login-password --region us-west-2 | docker login -u AWS --password-stdin https://546150905175.dkr.ecr.us-west-2.amazonaws.com;
docker pull 546150905175.dkr.ecr.us-west-2.amazonaws.com/kinesis-video-producer-sdk-cpp-amazon-linux:latest

次に以下のコマンドでコンテナを立ち上げてログインします。

sudo docker run -it --network="host" 546150905175.dkr.ecr.us-west-2.amazonaws.com/kinesis-video-producer-sdk-cpp-amazon-linux /bin/bash

ログインしたら以下のコマンドで環境変数を設定します。
PATH の方はそのまま実行で OK で、クレデンシャル情報は先ほど作成した IAM ユーザーの情報に置き換えて実行して下さい。

# PATH Config
export LD_LIBRARY_PATH=/opt/awssdk/amazon-kinesis-video-streams-producer-sdk-cpp/kinesis-video-native-build/downloads/local/lib:$LD_LIBRARY_PATH;
export PATH=/opt/awssdk/amazon-kinesis-video-streams-producer-sdk-cpp/kinesis-video-native-build/downloads/local/bin:$PATH;
export GST_PLUGIN_PATH=/opt/awssdk/amazon-kinesis-video-streams-producer-sdk-cpp/kinesis-video-native-build/downloads/local/lib:$GST_PLUGIN_PATH;

# Credential Config
export AWS_DEFAULT_REGION=ap-northeast-1;
export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX;
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;

Docker 側の準備はこれで OK です。

ホスト

次にホスト側の準備です。
ホストで gstreamer を実行する必要があるため、インストールします。

brew install gstreamer

データ容量が大きいためかちょっと時間かかるので、気長にお待ちください。

ホスト側の準備はこれで完了です。

配信コマンド

まずはホスト上で以下のコマンドを実行します。

gst-launch-1.0 \
  avfvideosrc device-index=1 ! \
  videoconvert ! \
  video/x-raw,width=1280,height=720 ! \
  vtenc_h264_hw allow-frame-reordering=FALSE realtime=TRUE \
  max-keyframe-interval=45 bitrate=512 ! \
  gdppay ! \
  tcpserversink host=127.0.0.1 sync=false
コマンドの補足
  • avfvideosrc device-index=1: macOS の Web カメラから映像を取得(device-index=1 は外部 USB カメラを指定)
  • videoconvert: 映像フォーマットを変換
  • video/x-raw,width=1280,height=720: 解像度を 1280x720 に設定
  • vtenc_h264_hw: Apple の Hardware エンコーダーを使用して H.264 でエンコード
    • allow-frame-reordering=FALSE: フレームの並び替えを無効化(遅延削減)
    • realtime=TRUE: リアルタイム配信モードを有効化
    • max-keyframe-interval=45: キーフレーム間隔を 45 フレームに設定
    • bitrate=512: ビットレートを 512 kbps に設定
  • gdppay: GDP(GStreamer Data Protocol)形式でパケット化
  • tcpserversink: TCP サーバーとして映像データを配信
    • host=127.0.0.1: ローカルホストで待機
    • sync=false: 同期を無効化(リアルタイム配信のため)

次に Docker 上で以下のコマンドを実行します。

gst-launch-1.0 -v \
  tcpclientsrc host=host.docker.internal ! \
  gdpdepay ! \
  video/x-h264, format=avc,alignment=au ! \
  h264parse ! \
  kvssink stream-name=dockerStream
コマンドの補足
  • gst-launch-1.0 -v: GStreamer パイプラインを詳細ログ付きで実行
  • tcpclientsrc host=host.docker.internal: ホスト側の TCP サーバーに接続
  • gdpdepay: GDP 形式のパケットを解析
  • video/x-h264, format=avc,alignment=au: H.264 ストリームの形式を指定
    • format=avc: AVC(Advanced Video Coding)形式
    • alignment=au: Access Unit 単位でアライメント
  • h264parse: H.264 ストリームを解析
  • kvssink stream-name=dockerStream: Kinesis Video Streams へ配信

ここまででエラーが発生しなければ、Kinesis Video Streams のメディア再生から、映像が確認できるはずです。
あとはセミをカーテンにデプロイして、Web カメラをセミの幼虫に向けてセッティングして、羽化が始まりそうになるまで待ちましょう。

1000001327

配信開始

さてセミの様子が大人しくなってきたら、いよいよ羽化の時間です。
先ほどのコマンドを実行して KVS 側で正常に映像が配信されることを確認したら、あとはセミを見ながら優雅なひと時をお過ごしください。

スクリーンショット 2025-07-18 21.10.31

自由研究 結果

自由研究らしく、簡単に結果をまとめておきます。

正直セミは子供の頃に知り尽くしている気持ちでいましたが、
改めて観察してみるといろんな発見があって脳みそのリフレッシュになりました。

セミになるまでの流れ

21:37 - 羽化開始

スクリーンショット 2025-07-18 21.14.06

23:00 - 羽化終了

スクリーンショット 2025-07-18 21.15.12

01:00 - 体が3割ぐらい乾き

スクリーンショット 2025-07-18 21.16.17

03:00 - 体が半乾き

スクリーンショット 2025-07-18 21.16.57

06:45 - 体が乾燥完了してフェードアウト

スクリーンショット 2025-07-18 21.18.23

07:00 - 朝起きた時、すごい神々しい感じになっていました

1000001332

気づいたこと

  • 意外と羽化のスピードが早い
  • 羽化の最中、結構激しく動いていて落ちないか心配になる
  • 羽化直後の薄緑色がとても美しい、実質妖精
  • 朝方に羽化するイメージだったけど、普通に真夜中に羽化が完了した、これは個体差あるかも
  • 羽化より、羽化後に体が固まるまでの方が時間がかかる

反省点

  • 映像がガビガビすぎる。次はgst-launch-1.0コマンドのオプションで画質を調整してみる
  • 映像だけだと時間がわかりづらいので、置き時計を設置した方がいい
  • 観察結果が 1 匹だけであり相対的な比較ができないため、より多くのセミの幼虫を用意した方がいい

補足

羽化したセミは翌日元いた日比谷公園で解放しています。
そのためセミは無事です、ご安心ください。

1000001333

さいごに

以上、セミの羽化をAmazon Kinesis Video Streamsへリアルタイムに配信してみました。
まさか人生でセミの羽化を AWS へ配信する日が来るとは思いませんでしたが、やってみると意外と楽しく、公式のDocker Imageも素直に動いてくれたため思いの外簡単にできました。

そして何より、普段は画面の向こう側にいるクラウドサービスが、リアルな自然現象と繋がった瞬間は、なんだか不思議で感動的でした。
セミも AWS も、どちらも夏の風物詩として記憶に残りそうです。

あと最近は小学生からプログラミングの授業があると聞きますし、自由研究の題材としても面白いんじゃないでしょうか。
「僕の夏休みの自由研究は AWS を使ったセミの観察です」なんて言ったら、先生も同級生もびっくりすること間違いなしです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.