🚀

分散型ストリーミングに挑むEthereumとIPFS連携手法

に公開

1. はじめに

近年、中央集権的なプラットフォームに依存しない、透明性・検閲耐性・所有権の担保された分散型サービスが注目されています。その中でも動画ストリーミングは、帯域・コスト・権利管理など高度な課題の多い領域です。本記事では、Ethereumスマートコントラクトと**IPFS(InterPlanetary File System)**を連携させることで、中央サーバーに頼らず動画を配信・管理する分散型動画ストリーミングプラットフォームの設計・実装手法を徹底解説します。

この記事は、個人開発者・スタートアップのテクニカルリーダーを対象に、公式ドキュメントには載っていない実践的な設計パターン・コーディング例・ベストプラクティスを豊富に盛り込みます。
読者が得られるメリット:

  • EthereumとIPFSの連携構築ノウハウと実用的なコード例
  • 分散型ストリーミングの設計思想とアーキテクチャ理解
  • セキュリティ・パフォーマンス・コスト最適化の具体的Tips
  • 他の技術との比較・将来性に関する深い知見

参考


2. 基礎:分散型ストリーミングのアーキテクチャと技術スタック

2.1 主要な技術要素

  • Ethereumスマートコントラクト:動画メタデータ・所有権・アクセス制御の管理
  • IPFS:動画ファイルの分散保存と配信
  • フロントエンド:ユーザーインターフェース(例:React + web3.js/ethers.js)
  • バックエンドAPI(任意):トランザクション補助・ノード管理用
  • ウォレット連携(MetaMask等)

2.2 アーキテクチャ全体像

  • 動画本体はIPFSに保存され、**CID(Content Identifier)**で参照
  • 動画メタデータ・所有権情報はEthereumコントラクトに記録
  • フロントエンドは両者を仲介してストリーミング体験を提供

参考


3. 実践的実装ガイド

3.1 環境準備とセットアップ

必要ツール・バージョン

# 必要パッケージのインストール
npm init -y
npm install --save hardhat ethers ipfs-http-client
npx hardhat

IPFSノードはInfura等のパブリックゲートウェイ利用も可能ですが、ローカルノード(go-ipfs v0.22.x)を立てると開発がスムーズです。

参考


3.2 基本的な機能の実装

3.2.1 スマートコントラクト:動画メタデータ管理

contracts/VideoPortal.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VideoPortal {
    struct Video {
        address owner;
        string ipfsCid; // IPFSコンテンツID
        string title;
        string description;
        uint256 createdAt;
    }

    Video[] public videos;

    event VideoUploaded(address indexed owner, uint256 indexed videoId, string ipfsCid);

    function uploadVideo(string memory _ipfsCid, string memory _title, string memory _description) public {
        videos.push(Video({
            owner: msg.sender,
            ipfsCid: _ipfsCid,
            title: _title,
            description: _description,
            createdAt: block.timestamp
        }));
        emit VideoUploaded(msg.sender, videos.length - 1, _ipfsCid);
    }

    function getVideo(uint256 _videoId) public view returns (Video memory) {
        require(_videoId < videos.length, "Invalid videoId");
        return videos[_videoId];
    }

    function getVideosCount() public view returns (uint256) {
        return videos.length;
    }
}

デプロイ例(scripts/deploy.js

const hre = require("hardhat");

async function main() {
    const VideoPortal = await hre.ethers.getContractFactory("VideoPortal");
    const videoPortal = await VideoPortal.deploy();
    await videoPortal.deployed();
    console.log(`VideoPortal deployed to: ${videoPortal.address}`);
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});

参考


3.2.2 IPFSへ動画ファイルをアップロード

uploadVideo.js

const { create } = require('ipfs-http-client');
const fs = require('fs');

(async () => {
    // Infura IPFSノード(APIキー必須)
    const projectId = "YOUR_INFURA_PROJECT_ID";
    const projectSecret = "YOUR_INFURA_PROJECT_SECRET";
    const auth = 'Basic ' + Buffer.from(projectId + ':' + projectSecret).toString('base64');

    const ipfs = create({
        host: 'ipfs.infura.io',
        port: 5001,
        protocol: 'https',
        headers: {
            authorization: auth,
        },
    });

    const file = fs.readFileSync('./sample.mp4');
    const { cid } = await ipfs.add({ content: file });

    console.log('IPFS CID:', cid.toString());
})();

参考


3.2.3 フロントエンド:動画アップロード&再生(React例)

src/App.js

import React, { useState } from "react";
import { ethers } from "ethers";
import { create } from "ipfs-http-client";
import VideoPortalABI from "./abi/VideoPortal.json";

const CONTRACT_ADDRESS = "YOUR_DEPLOYED_CONTRACT_ADDRESS";
const INFURA_PROJECT_ID = "YOUR_INFURA_PROJECT_ID";
const INFURA_PROJECT_SECRET = "YOUR_INFURA_PROJECT_SECRET";
const IPFS_GATEWAY = "https://ipfs.io/ipfs/";

const ipfs = create({
  host: "ipfs.infura.io",
  port: 5001,
  protocol: "https",
  headers: {
    authorization:
      "Basic " +
      Buffer.from(INFURA_PROJECT_ID + ":" + INFURA_PROJECT_SECRET).toString(
        "base64"
      ),
  },
});

function App() {
  const [file, setFile] = useState(null);
  const [videos, setVideos] = useState([]);

  const uploadToIPFS = async (file) => {
    const { cid } = await ipfs.add(file);
    return cid.toString();
  };

  const uploadVideo = async () => {
    if (!window.ethereum || !file) return;
    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const contract = new ethers.Contract(CONTRACT_ADDRESS, VideoPortalABI, signer);

    const cid = await uploadToIPFS(file);
    const tx = await contract.uploadVideo(cid, "タイトル例", "説明文例");
    await tx.wait();
    alert("アップロード完了");
  };

  const fetchVideos = async () => {
    const provider = new ethers.BrowserProvider(window.ethereum);
    const contract = new ethers.Contract(CONTRACT_ADDRESS, VideoPortalABI, provider);
    const count = await contract.getVideosCount();
    let items = [];
    for (let i = 0; i < count; i++) {
      const v = await contract.getVideo(i);
      items.push(v);
    }
    setVideos(items);
  };

  return (
    <div>
      <h1>分散型動画ストリーミングPoC</h1>
      <input type="file" accept="video/mp4" onChange={e => setFile(e.target.files[0])} />
      <button onClick={uploadVideo}>動画アップロード</button>
      <button onClick={fetchVideos}>動画一覧取得</button>
      <ul>
        {videos.map((v, i) => (
          <li key={i}>
            <h3>{v.title}</h3>
            <video width="320" controls src={IPFS_GATEWAY + v.ipfsCid}></video>
            <div>説明: {v.description}</div>
            <div>所有者: {v.owner}</div>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

参考


3.3 応用的な機能の実装

3.3.1 アクセスコントロール付き動画配信(有料視聴/許可制)

スマートコントラクト(有料視聴例)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract PaidVideoPortal {
    struct Video { /* 先述と同様 */ }
    Video[] public videos;
    mapping(uint256 => mapping(address => bool)) public paidUsers;

    function uploadVideo(string memory _ipfsCid, string memory _title, string memory _desc) public { /* 省略 */ }

    function payToView(uint256 _videoId) public payable {
        require(msg.value >= 0.01 ether, "Fee not enough");
        paidUsers[_videoId][msg.sender] = true;
        // クリエイターへ分配(例: 95%)
        payable(videos[_videoId].owner).transfer(msg.value * 95 / 100);
    }

    function canView(uint256 _videoId, address _user) public view returns (bool) {
        return paidUsers[_videoId][_user] || videos[_videoId].owner == _user;
    }
}

React(視聴権チェック)

const canView = await contract.canView(videoId, userAddress);
if (canView) {
    // <video src=...>
} else {
    // 「視聴料を支払ってください」ボタン表示
}

課題と対策:

  • IPFSの動画自体は公開なので、オンチェーンでのCID暗号化/復号キー管理分割配信+署名検証などを検討(詳細は考察セクション参照)

参考


4. Tips & ベストプラクティス

  1. CIDの検証と改ざん対策

    • コントラクトでCIDのフォーマットや重複登録をチェック
    • メタデータのSHA256ハッシュも併記することで不正アップロード検出
  2. 動画ファイルの分割とストリーミング最適化

    • HLS/m3u8等で動画をチャンク分割し、IPFSマニ

自動レビュー結果 (2025-07-27 00:59)

  • 記事品質: 4.5/5.0
  • トピック多様性: 5.0/5.0
  • コードサンプル: 4.6/5.0
  • 実用性・応用性: 4.4/5.0
  • 総合評価: 4.6/5.0 (優秀)

Discussion