356 Views
February 01, 24
スライド概要
GDG Tokyo Monthly Online Tech Talksで登壇した内容です。
iOSアプリの開発をしています。 個人アプリも色々あります。 # Type: https://type-markdown.app WebCollector: https://webcollector.app/ Pity: https://freetimepicker.firebaseapp.com
Firebaseで動画共有アプリ @fromkk かっくん
kyuとは https://kyu-o.com/ • TranSe Inc.が運営しているイメージングブランド • 過去にはバックパック、クロス、SDカードスリーブを生産・販売 • 今はカメラ・アプリの開発に注力中
kyu app https://kyu-o.com/pages/app • 現在ベータ版で一部の方のみ公開中 • 簡単にいうと部屋を作って友だちを招待して動画を撮影して共有するアプリ • 動画は1動画9秒制限、1日に9つまでアップロード可能 • 動画は一度下書きに保存され、6時間に一度公開される • Highlight機能でまとめ動画を生成する
活用しているFirebase • Firestore • Storage • Authentication • Functions • Messaging • Remote Con g • Crashlytics fi • etc…
動画をStorageで活用する • 動画をアプリで活用するためには動画だけ があればいいというわけではない • サムネイル画像、動画の長さなどのメタ 情報が必要になる • サムネイルは動画から切り出す必要がある • 動画がStorageに上がったら、 mpeg ff で切り出す
サムネイル画像の生成・保存フロー
動画から画像を抜き出す準備 ff fl ff ff npm install @ mpeg-installer/ mpeg uent- mpeg
動画から画像を抜き出すコード
/**
* generate screen shot
* @param {string} videoFilePath local video path
* @param {string} newFileName thumbnail file name
* @return {Promise}
*/
async function takeScreenshot(videoFilePath, newFileName) {
return new Promise((resolve, reject) => {
ffmpeg({source: videoFilePath})
.on("filenames", (filenames) => {})
.on("end", () => {
resolve(null);
})
.on("error", (error) => {
reject(error);
})
.screenshots({
count: 1,
timestamps: [0],
filename: newFileName,
folder: os.tmpdir(),
})
.aspect(1);
});
}
Highlight動画を作成する • 部屋のオーナーは全ての動画が公開され たらまとめ動画を生成する • 動画の真ん中の1秒を取得して繋げる • Google CloudのTranscoder APIを利用 している • Storageと互換性があるので使いやすい
Highlight動画の生成・保存フロー
動画を繋ぎ合わせる準備 https://cloud.google.com/transcoder/docs
動画を繋ぎ合わせるコード Pt.1
const {TranscoderServiceClient} = require("@google-cloud/video-transcoder").v1;
const client = new TranscoderServiceClient();
/**
* calcOffsetNanoSec
* @param {number} offsetValueFractionalSecs input
* @return {number}
*/
function calcOffsetNanoSec(offsetValueFractionalSecs) {
if (offsetValueFractionalSecs.toString().indexOf(".") !== -1) {
return (
1000000000 *
Number("." + offsetValueFractionalSecs.toString().split(".")[1])
);
}
return 0;
}
/** @typedef {{start: number, end: number}} startAndEndResult */
/**
* get start and end from duration and minimum length.
* @param {number} duration
* @param {number} minimumLength
* @return {startAndEndResult}
*/
function startAndEnd(duration, minimumLength) {
if (duration > minimumLength) {
const halfDuration = duration / 2;
const halfMinimumLength = minimumLength / 2;
return {
"start": halfDuration - halfMinimumLength,
"end": halfDuration + halfMinimumLength - 0.01,
};
} else {
return {
"start": 0, "end": duration - 0.01,
};
}
}
動画を繋ぎ合わせるコード Pt.2
/**
* @typedef {{input: string, start: number, end: number}} resource
*/
/**
* create job
* @param {resource[]} resources inputs
* @param {string} output url
*/
async function createJob(resources, output) {
const inputs = resources.map((resource, index) => {
return {
key: `input${index + 1}`,
uri: resource.input,
};
});
const editList = resources.map((resource, index) => {
const startTimeOffsetSec = Math.trunc(resource.start);
const startTimeOffsetNanoSec = calcOffsetNanoSec(resource.start);
const endTimeOffsetSec = Math.trunc(resource.end);
const endTimeOffsetNanoSec = calcOffsetNanoSec(resource.end);
return {
key: `atom${index + 1}`,
inputs: [`input${index + 1}`],
startTimeOffset: {
seconds: startTimeOffsetSec,
nanos: startTimeOffsetNanoSec,
},
endTimeOffset: {
seconds: endTimeOffsetSec,
nanos: endTimeOffsetNanoSec,
},
};
});
const request = {
parent: client.locationPath(projectId, location),
job: {
outputUri: output,
config: {
inputs: inputs,
editList: editList,
elementaryStreams: [
{
key: "video-stream0",
videoStream: {
h264: {
heightPixels: 1024,
widthPixels: 1024,
bitrateBps: 1100000,
frameRate: 30,
},
},
},
{
key: "audio-stream0",
audioStream: {
codec: "aac",
bitrateBps: 64000,
},
},
],
muxStreams: [
{
key: "hd",
container: "mp4",
elementaryStreams: ["video-stream0", "audio-stream0"],
},
],
},
},
};
}
// Run request
const [response] = await client.createJob(request);
console.log(`Job: ${response.name}`);
動画を繋ぎ合わせるコード Pt.3
/** @type {resource[]} */
const resources = contents.map((content) => {
/** @type {number} */
const duration = startAndEnd(content.duration, 1);
return {
"input": `gs://your-project.appspot.com/${content.path}`,
"start": duration.start,
"end": duration.end,
};
});
const outputUrl = `gs://your-project.appspot.com/highlight/`;
await createJob(resources, outputUrl);
今後したいこと • 動画の選択・動画の中でもHighlightに選択する場所をイイ感じにしたい • 画像解析や音声解析などの技術が想定されるが、どのように実現できるの か不透明 • 物理カメラとの連携機能が必要 • ハードウェア連携の方法が不透明 • etc…
We are hiring • 今後したいことに興味が出た方 • カメラや動画アプリの開発に興味が出た方 いましたらお声がけください!
まとめ • 動画共有アプリの開発にFirebaseをフル活用しています • Firebaseだけでは開発できない機能は別途ライブラリをインストールした り、Google Cloudの機能を利用しています • こんなサービスへの開発に興味が出た方がいましたらお声がけください
自己紹介 • 名前:植岡 和哉(かっくん) • SNS: @fromkk (X, note, GitHub, Qiita, Zenn, etc…) • フリーランスのiOSデベロッパー • 埼玉県所沢市に中古マンションを購入・リノベーションして住 んでいます • 猫が2匹います • 主な趣味は写真です。最近編み物始めました。