使用IPFS实现去中心化视频共享应用
1. 视频点赞逻辑与测试
当一个账户对视频点赞时,点赞者的代币余额减1,视频上传者的代币余额加1,同时智能合约记录该账户已点赞此视频,视频的总点赞数加1。若另一个账户再点赞该视频,同样的操作会再次执行,视频总点赞数变为2。要确保点赞视频的事件被触发,并且一个用户不能对同一视频多次点赞。
执行测试的命令如下:
(videos-venv) $ py.test tests/test_videos_sharing.py
2. 启动私有以太坊区块链
使用 geth 启动私有以太坊区块链,不使用Ganache是因为其稳定版本尚不支持事件(不过Beta 2.0.0版本已支持)。具体操作步骤如下:
1. 进入项目目录并创建新链:
(videos-venv) $ cd videos_sharing_smart_contract
(videos-venv) $ populus chain new localblock
(videos-venv) $ ./chains/localblock/init_chain.sh
- 编辑
chains/localblock/run_chain.sh文件,将--ipcpath的值改为/tmp/geth.ipc。 - 编辑
project.json文件,在chains对象中添加localblock键:
"localblock": {
"chain": {
"class": "populus.chain.ExternalChain"
},
"web3": {
"provider": {
"class": "web3.providers.ipc.IPCProvider",
"settings": {
"ipc_path":"/tmp/geth.ipc"
}
}
},
"contracts": {
"backends": {
"JSONFile": {"$ref": "contracts.backends.JSONFile"},
"ProjectContracts": {
"$ref": "contracts.backends.ProjectContracts"
}
}
}
}
- 启动区块链:
(videos-venv) $ ./chains/localblock/run_chain.sh
- 编译智能合约:
(videos-venv) $ populus compile
- 将智能合约部署到私有区块链:
(videos-venv) $ populus deploy --chain localblock VideosSharing
将智能合约部署的地址记录在 address.txt 文件中,该文件需与 videos_sharing_smart_contract 目录相邻。
3. 创建引导脚本
引导脚本用于加载数据,方便应用开发。可以从 https://videos.pexels.com/ 下载免费视频,在 videos_sharing_smart_contract 目录相邻处创建 stock_videos 目录,并将下载的MP4文件放入该目录。
创建 bootstrap_videos.py 脚本,完整代码可参考 此处 :
import os, json
import ipfsapi
from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt
w3 = Web3(IPCProvider('/tmp/geth.ipc'))
common_password = 'bitcoin123'
accounts = []
with open('accounts.txt', 'w') as f:
for i in range(4):
account = w3.personal.newAccount(common_password)
accounts.append(account)
f.write(account + "\n")
with open('address.txt', 'r') as f:
address = f.read().rstrip("\n")
with open('videos_sharing_smart_contract/build/contracts.json') as f:
contract = json.load(f)
abi = contract['VideosSharing']['abi']
VideosSharing = w3.eth.contract(address=address, abi=abi)
c = ipfsapi.connect()
coinbase = w3.eth.accounts[0]
coinbase_password = 'this-is-not-a-secure-password'
# 转移以太币
for destination in accounts:
nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(coinbase))
txn = {
'from': coinbase,
'to': Web3.toChecksumAddress(destination),
'value': w3.toWei('100', 'ether'),
'gas': 70000,
'gasPrice': w3.toWei('1', 'gwei'),
'nonce': nonce
}
txn_hash = w3.personal.sendTransaction(txn, coinbase_password)
wait_for_transaction_receipt(w3, txn_hash)
# 转移ERC20代币
for destination in accounts:
nonce = w3.eth.getTransactionCount(coinbase)
txn = VideosSharing.functions.transfer(destination, 100).buildTransaction({
'from': coinbase,
'gas': 70000,
'gasPrice': w3.toWei('1', 'gwei'),
'nonce': nonce
})
txn_hash = w3.personal.sendTransaction(txn, coinbase_password)
wait_for_transaction_receipt(w3, txn_hash)
# 上传视频
directory = 'stock_videos'
movies = os.listdir(directory)
length_of_movies = len(movies)
for index, movie in enumerate(movies):
account = accounts[index//7]
ipfs_add = c.add(directory + '/' + movie)
ipfs_path = ipfs_add['Hash'].encode('utf-8')
title = movie.rstrip('.mp4')[:20].encode('utf-8')
nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(account))
txn = VideosSharing.functions.upload_video(ipfs_path, title).buildTransaction({
'from': account,
'gas': 200000,
'gasPrice': w3.toWei('30', 'gwei'),
'nonce': nonce
})
txn_hash = w3.personal.sendTransaction(txn, common_password)
wait_for_transaction_receipt(w3, txn_hash)
4. 引导脚本执行步骤
- 启动私有区块链和IPFS守护进程:
$ ipfs daemon
若不知道如何安装和启动IPFS,可参考相关资料。
2. 在虚拟环境中安装IPFS Python库:
(videos-venv) $ pip install ipfsapi
- 运行引导脚本:
(videos-venv) $ python bootstrap_videos.py
可以通过创建 check_bootstrap.py 脚本来测试引导脚本是否成功:
import json
from web3 import Web3, IPCProvider
w3 = Web3(IPCProvider('/tmp/geth.ipc'))
with open('accounts.txt', 'r') as f:
account = f.readline().rstrip("\n")
with open('address.txt', 'r') as f:
address = f.read().rstrip("\n")
with open('videos_sharing_smart_contract/build/contracts.json') as f:
contract = json.load(f)
abi = contract['VideosSharing']['abi']
VideosSharing = w3.eth.contract(address=address, abi=abi)
print(VideosSharing.functions.latest_videos_index(account).call())
运行该脚本,若输出为0,则引导脚本失败;若输出不为0,则视频信息已成功上传到区块链。
5. 构建视频共享Web应用
使用Django库构建智能合约的前端应用,具体步骤如下:
1. 安装Django:
(videos-venv) $ pip install Django
- 安装OpenCV Python库以获取视频缩略图:
(videos-venv) $ pip install opencv-python
- 创建Django项目目录:
(videos-venv) $ django-admin startproject decentralized_videos
- 在项目目录下创建静态媒体目录:
(videos-venv) $ cd decentralized_videos
(videos-venv) $ mkdir static media
- 创建名为
videos的Django应用:
(videos-venv) $ python manage.py startapp videos
- 更新Django项目设置文件
decentralized_videos/settings.py:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'videos'
]
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
6. 视图函数
视图函数类似于API端点,以下是主要视图函数的说明:
from django.shortcuts import render, redirect
from videos.models import videos_sharing
def index(request):
videos = videos_sharing.recent_videos()
context = {'videos': videos}
return render(request, 'videos/index.html', context)
def channel(request, video_user):
videos = videos_sharing.get_videos(video_user)
context = {'videos': videos, 'video_user': video_user}
return render(request, 'videos/channel.html', context)
def video(request, video_user, index):
video = videos_sharing.get_video(video_user, index)
context = {'video': video}
return render(request, 'videos/video.html', context)
def upload(request):
context = {}
if request.POST:
video_user = request.POST['video_user']
title = request.POST['title']
video_file = request.FILES['video_file']
password = request.POST['password']
videos_sharing.upload_video(video_user, password, video_file, title)
context['upload_success'] = True
return render(request, 'videos/upload.html', context)
def like(request):
video_user = request.POST['video_user']
index = int(request.POST['index'])
password = request.POST['password']
video_liker = request.POST['video_liker']
videos_sharing.like_video(video_liker, password, video_user, index)
return redirect('video', video_user=video_user, index=index)
7. 模型类
模型类 VideosSharing 包含了主要的逻辑和操作,以下是其核心代码:
import os.path, json
import ipfsapi
import cv2
from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt
from decentralized_videos.settings import STATICFILES_DIRS, STATIC_URL, BASE_DIR, MEDIA_ROOT
class VideosSharing:
def __init__(self):
self.w3 = Web3(IPCProvider('/tmp/geth.ipc'))
with open('../address.txt', 'r') as f:
address = f.read().rstrip("\n")
with open('../videos_sharing_smart_contract/build/contracts.json') as f:
contract = json.load(f)
abi = contract['VideosSharing']['abi']
self.SmartContract = self.w3.eth.contract(address=address, abi=abi)
self.ipfs_con = ipfsapi.connect()
def recent_videos(self, amount=20):
events = self.SmartContract.events.UploadVideo.createFilter(fromBlock=0).get_all_entries()
videos = []
for event in events:
video = {}
video['user'] = event['args']['_user']
video['index'] = event['args']['_index']
video['path'] = self.get_video_path(video['user'], video['index'])
video['title'] = self.get_video_title(video['user'], video['index'])
video['thumbnail'] = self.get_video_thumbnail(video['path'])
videos.append(video)
videos.reverse()
return videos[:amount]
def get_videos(self, user, amount=20):
latest_index = self.SmartContract.functions.latest_videos_index(user).call()
i = 0
videos = []
while i < amount and i < latest_index:
video = {}
index = latest_index - i - 1
video['user'] = user
video['index'] = index
video['path'] = self.get_video_path(user, index)
video['title'] = self.get_video_title(user, index)
video['thumbnail'] = self.get_video_thumbnail(video['path'])
videos.append(video)
i += 1
return videos
def get_video_path(self, user, index):
return self.SmartContract.functions.videos_path(user, index).call().decode('utf-8')
def get_video_title(self, user, index):
return self.SmartContract.functions.videos_title(user, index).call().decode('utf-8')
def get_video_thumbnail(self, ipfs_path):
thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
url_file = STATIC_URL + '/' + ipfs_path + '.png'
if os.path.isfile(thumbnail_file):
return url_file
else:
return "https://bulma.io/images/placeholders/640x480.png"
def get_video(self, user, index):
video = {}
ipfs_path = self.get_video_path(user, index)
video_title = self.get_video_title(user, index)
video_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4'
thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
video['title'] = video_title
video['user'] = user
video['index'] = index
video['aggregate_likes'] = self.SmartContract.functions.video_aggregate_likes(user, index).call()
if os.path.isfile(video_file):
video['url'] = STATIC_URL + '/' + ipfs_path + '.mp4'
else:
self.ipfs_con.get(ipfs_path)
os.rename(BASE_DIR + '/' + ipfs_path, STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4')
video['url'] = STATIC_URL + '/' + ipfs_path + '.mp4'
if not os.path.isfile(thumbnail_file):
self.process_thumbnail(ipfs_path)
return video
def upload_video(self, video_user, password, video_file, title):
video_path = MEDIA_ROOT + '/video.mp4'
with open(video_path, 'wb+') as destination:
for chunk in video_file.chunks():
destination.write(chunk)
ipfs_add = self.ipfs_con.add(video_path)
ipfs_path = ipfs_add['Hash'].encode('utf-8')
title = title[:20].encode('utf-8')
nonce = self.w3.eth.getTransactionCount(Web3.toChecksumAddress(video_user))
txn = self.SmartContract.functions.upload_video(ipfs_path, title).buildTransaction({
'from': video_user,
'gas': 200000,
'gasPrice': self.w3.toWei('30', 'gwei'),
'nonce': nonce
})
txn_hash = self.w3.personal.sendTransaction(txn, password)
wait_for_transaction_receipt(self.w3, txn_hash)
def process_thumbnail(self, ipfs_path):
thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
if not os.path.isfile(thumbnail_file):
video_path = STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4'
cap = cv2.VideoCapture(video_path)
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
_, frame = cap.read()
cv2.imwrite(thumbnail_file, frame)
def like_video(self, video_liker, password, video_user, index):
if self.SmartContract.functions.video_has_been_liked(video_liker, video_user, index).call():
return
nonce = self.w3.eth.getTransactionCount(Web3.toChecksumAddress(video_liker))
txn = self.SmartContract.functions.like_video(video_user, index).buildTransaction({
'from': video_liker,
'gas': 200000,
'gasPrice': self.w3.toWei('30', 'gwei'),
'nonce': nonce
})
txn_hash = self.w3.personal.sendTransaction(txn, password)
wait_for_transaction_receipt(self.w3, txn_hash)
videos_sharing = VideosSharing()
通过以上步骤,我们可以使用IPFS和以太坊智能合约实现一个去中心化的视频共享应用。该应用允许用户上传视频、点赞视频,并通过区块链记录相关信息,同时利用IPFS存储视频文件,提高了数据的安全性和可靠性。
以下是创建私有以太坊区块链和运行引导脚本的流程图:
graph TD;
A[启动私有以太坊区块链] --> B[进入项目目录并创建新链];
B --> C[编辑脚本文件修改IPCPATH];
C --> D[编辑project.json文件添加localblock];
D --> E[启动区块链];
E --> F[编译智能合约];
F --> G[部署智能合约并记录地址];
G --> H[启动IPFS守护进程];
H --> I[安装IPFS Python库];
I --> J[运行引导脚本];
J --> K[测试引导脚本是否成功];
以下是主要操作步骤的表格总结:
| 操作 | 步骤 | 命令示例 |
| ---- | ---- | ---- |
| 启动私有以太坊区块链 | 进入项目目录、创建新链、编辑文件、启动链、编译和部署合约 | cd videos_sharing_smart_contract 等 |
| 运行引导脚本 | 启动IPFS、安装库、运行脚本、测试 | ipfs daemon 等 |
| 构建Web应用 | 安装库、创建项目和应用、更新设置 | pip install Django 等 |
使用IPFS实现去中心化视频共享应用
8. 模型类方法详解
- 初始化方法
__init__:该方法用于初始化VideosSharing类的实例。首先创建一个Web3连接对象w3,连接到本地的以太坊节点。接着从address.txt文件中读取智能合约的地址,从contracts.json文件中读取智能合约的ABI(应用二进制接口),并使用w3.eth.contract方法创建智能合约对象SmartContract。最后,使用ipfsapi.connect方法创建一个 IPFS 连接对象ipfs_con。
def __init__(self):
self.w3 = Web3(IPCProvider('/tmp/geth.ipc'))
with open('../address.txt', 'r') as f:
address = f.read().rstrip("\n")
with open('../videos_sharing_smart_contract/build/contracts.json') as f:
contract = json.load(f)
abi = contract['VideosSharing']['abi']
self.SmartContract = self.w3.eth.contract(address=address, abi=abi)
self.ipfs_con = ipfsapi.connect()
- 获取近期视频方法
recent_videos:该方法用于获取近期上传的视频。通过创建一个UploadVideo事件过滤器,从区块链的起始块开始获取所有的UploadVideo事件。对于每个事件,提取视频的上传者、索引、路径、标题和缩略图信息,并将这些信息存储在一个字典中,最后将这些字典添加到videos列表中。为了获取近期视频,将列表反转并返回前amount个视频。
def recent_videos(self, amount=20):
events = self.SmartContract.events.UploadVideo.createFilter(fromBlock=0).get_all_entries()
videos = []
for event in events:
video = {}
video['user'] = event['args']['_user']
video['index'] = event['args']['_index']
video['path'] = self.get_video_path(video['user'], video['index'])
video['title'] = self.get_video_title(video['user'], video['index'])
video['thumbnail'] = self.get_video_thumbnail(video['path'])
videos.append(video)
videos.reverse()
return videos[:amount]
- 获取特定用户视频方法
get_videos:该方法用于获取特定用户上传的视频。首先通过智能合约的latest_videos_index方法获取该用户上传视频的最新索引。然后从最新索引开始,依次获取视频的信息,直到达到指定的数量amount或没有更多视频为止。
def get_videos(self, user, amount=20):
latest_index = self.SmartContract.functions.latest_videos_index(user).call()
i = 0
videos = []
while i < amount and i < latest_index:
video = {}
index = latest_index - i - 1
video['user'] = user
video['index'] = index
video['path'] = self.get_video_path(user, index)
video['title'] = self.get_video_title(user, index)
video['thumbnail'] = self.get_video_thumbnail(video['path'])
videos.append(video)
i += 1
return videos
- 获取视频路径和标题方法
get_video_path和get_video_title:这两个方法分别用于获取视频的路径和标题。通过调用智能合约的videos_path和videos_title方法,并将结果进行解码。
def get_video_path(self, user, index):
return self.SmartContract.functions.videos_path(user, index).call().decode('utf-8')
def get_video_title(self, user, index):
return self.SmartContract.functions.videos_title(user, index).call().decode('utf-8')
- 获取视频缩略图方法
get_video_thumbnail:该方法用于获取视频的缩略图。首先检查本地是否存在该视频的缩略图文件,如果存在则返回缩略图的 URL,否则返回一个占位图的 URL。
def get_video_thumbnail(self, ipfs_path):
thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
url_file = STATIC_URL + '/' + ipfs_path + '.png'
if os.path.isfile(thumbnail_file):
return url_file
else:
return "https://bulma.io/images/placeholders/640x480.png"
- 获取特定视频方法
get_video:该方法用于获取特定视频的信息。首先获取视频的路径、标题、文件、缩略图和总点赞数。然后检查本地是否存在该视频文件,如果不存在则从 IPFS 中下载该文件,并将其移动到静态文件目录。最后,如果缩略图不存在,则调用process_thumbnail方法生成缩略图。
def get_video(self, user, index):
video = {}
ipfs_path = self.get_video_path(user, index)
video_title = self.get_video_title(user, index)
video_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4'
thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
video['title'] = video_title
video['user'] = user
video['index'] = index
video['aggregate_likes'] = self.SmartContract.functions.video_aggregate_likes(user, index).call()
if os.path.isfile(video_file):
video['url'] = STATIC_URL + '/' + ipfs_path + '.mp4'
else:
self.ipfs_con.get(ipfs_path)
os.rename(BASE_DIR + '/' + ipfs_path, STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4')
video['url'] = STATIC_URL + '/' + ipfs_path + '.mp4'
if not os.path.isfile(thumbnail_file):
self.process_thumbnail(ipfs_path)
return video
- 上传视频方法
upload_video:该方法用于上传视频。首先将视频文件保存到媒体目录,然后将该文件添加到 IPFS 中,获取 IPFS 路径。接着将视频标题进行处理,调用智能合约的upload_video方法上传视频信息,并等待交易确认。
def upload_video(self, video_user, password, video_file, title):
video_path = MEDIA_ROOT + '/video.mp4'
with open(video_path, 'wb+') as destination:
for chunk in video_file.chunks():
destination.write(chunk)
ipfs_add = self.ipfs_con.add(video_path)
ipfs_path = ipfs_add['Hash'].encode('utf-8')
title = title[:20].encode('utf-8')
nonce = self.w3.eth.getTransactionCount(Web3.toChecksumAddress(video_user))
txn = self.SmartContract.functions.upload_video(ipfs_path, title).buildTransaction({
'from': video_user,
'gas': 200000,
'gasPrice': self.w3.toWei('30', 'gwei'),
'nonce': nonce
})
txn_hash = self.w3.personal.sendTransaction(txn, password)
wait_for_transaction_receipt(self.w3, txn_hash)
- 生成缩略图方法
process_thumbnail:该方法用于生成视频的缩略图。首先检查本地是否存在该视频的缩略图文件,如果不存在,则使用 OpenCV 库打开视频文件,读取第一帧并保存为缩略图文件。
def process_thumbnail(self, ipfs_path):
thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
if not os.path.isfile(thumbnail_file):
video_path = STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4'
cap = cv2.VideoCapture(video_path)
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
_, frame = cap.read()
cv2.imwrite(thumbnail_file, frame)
- 点赞视频方法
like_video:该方法用于点赞视频。首先检查该视频是否已经被点赞,如果已经被点赞则直接返回,否则调用智能合约的like_video方法进行点赞操作,并等待交易确认。
def like_video(self, video_liker, password, video_user, index):
if self.SmartContract.functions.video_has_been_liked(video_liker, video_user, index).call():
return
nonce = self.w3.eth.getTransactionCount(Web3.toChecksumAddress(video_liker))
txn = self.SmartContract.functions.like_video(video_user, index).buildTransaction({
'from': video_liker,
'gas': 200000,
'gasPrice': self.w3.toWei('30', 'gwei'),
'nonce': nonce
})
txn_hash = self.w3.personal.sendTransaction(txn, password)
wait_for_transaction_receipt(self.w3, txn_hash)
9. 注意事项
- 异常处理 :在实际应用中,应该添加更多的异常处理代码,以应对可能出现的网络问题、文件操作错误、智能合约调用失败等情况,提高应用的健壮性。
- 性能优化 :在获取近期视频和特定用户视频时,由于是从区块链的起始块开始获取所有事件,可能会导致性能问题。在实际应用中,可以考虑限制获取的块范围,或者将事件存储到数据库中,以提高查询效率。
- 安全性 :在处理用户输入时,应该进行严格的验证和过滤,防止 SQL 注入、XSS 攻击等安全问题。同时,在处理用户密码时,应该使用安全的加密算法进行存储和传输。
10. 总结
通过使用 IPFS 和以太坊智能合约,我们成功实现了一个去中心化的视频共享应用。该应用具有以下优点:
- 数据安全 :视频文件存储在 IPFS 中,利用 IPFS 的分布式存储特性,提高了数据的安全性和可靠性。
- 不可篡改 :视频的上传和点赞信息记录在区块链上,具有不可篡改的特性,保证了数据的真实性和完整性。
- 去中心化 :应用不依赖于单一的服务器,避免了单点故障和数据垄断的问题。
以下是模型类主要方法的调用关系流程图:
graph LR;
A[__init__] --> B[recent_videos];
A --> C[get_videos];
A --> D[get_video];
A --> E[upload_video];
A --> F[like_video];
B --> G[get_video_path];
B --> H[get_video_title];
B --> I[get_video_thumbnail];
C --> G;
C --> H;
C --> I;
D --> G;
D --> H;
D --> I;
D --> J[process_thumbnail];
E --> J;
以下是关键操作的资源消耗表格:
| 操作 | 气体消耗 | 备注 |
| ---- | ---- | ---- |
| 转移以太币 | 70000 | |
| 转移 ERC20 代币 | 70000 | |
| 上传视频 | 200000 | 需较高气体和气体价格 |
| 点赞视频 | 200000 | 需较高气体和气体价格 |
通过以上的步骤和优化建议,我们可以进一步完善这个去中心化视频共享应用,使其更加稳定、高效和安全。
超级会员免费看
1882

被折叠的 条评论
为什么被折叠?



