스마트시대

26 VIDEO UPLOAD 26.1 VideosRepository 26.2 uploadVideoProvider 26.3 Cloud Functions 26.4 ffmpeg 26.5 publicUrl 본문

Programing/Flutter

26 VIDEO UPLOAD 26.1 VideosRepository 26.2 uploadVideoProvider 26.3 Cloud Functions 26.4 ffmpeg 26.5 publicUrl

스마트시대 2023. 6. 1. 18:58
728x90

import 'dart:io';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

//(26.1)
class VideosRepository {
  final FirebaseFirestore _db = FirebaseFirestore.instance;
  final FirebaseStorage _storage = FirebaseStorage.instance;

  // upload a video file

  UploadTask uploadVideoFile(File video, String uid) {
    final fileRef = _storage.ref().child(
          "/videos/$uid/${DateTime.now().millisecondsSinceEpoch.toString()}",
        );
    return fileRef.putFile(video);
  }

  // create a video document
}

final videosRepo = Provider((ref) => VideosRepository());

import 'dart:async';
import 'dart:io';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tiktok_clone/features/authentication/repos/authentication_repo.dart';
import 'package:tiktok_clone/features/videos/repos/videos_repo.dart';

//(26.1)
class UploadVideoViewModel extends AsyncNotifier<void> {
  late final VideosRepository _repository;

  @override
  FutureOr<void> build() {
    _repository = ref.read(videosRepo);
  }

  Future<void> uploadVideo(File video) async {
    final user = ref.read(authRepo).user;
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      await _repository.uploadVideoFile(
        video,
        user!.uid,
      );
    });
  }
}

 

class VideoModel {
  //String title;
  //VideoModel({required this.title});
  
  //(26.1)
  --------------------------------
  final String title;
  final String description;
  final String fileUrl;
  final String thumbnailUrl;
  final String creatorUid;
  final int likes;
  final int comments;
  final int createdAt;

  VideoModel({
    required this.title,
    required this.description,
    required this.fileUrl,
    required this.thumbnailUrl,
    required this.creatorUid,
    required this.likes,
    required this.comments,
    required this.createdAt,
  });
    --------------------------------
}

 

26.2 uploadVideoProvider

  final String fileUrl;
  final String thumbnailUrl;
  final String creatorUid;
  
  ------------------
  //(26.2)
  final String creator;
    ------------------
    
    
  final int likes;
  final int comments;
  final int createdAt;
@@ -17,5 +18,20 @@ class VideoModel {
    required this.likes,
    required this.comments,
    required this.createdAt,
    
    
      ------------------
        //(26.2)
    required this.creator,
      ------------------
  });



  ------------------
    //(26.2)
  Map<String, dynamic> toJson() {
    return {
      "title": title,
      "description": description,
      "fileUrl": fileUrl,
      "thumbnailUrl": thumbnailUrl,
      "creatorUid": creatorUid,
      "likes": likes,
      "comments": comments,
      "createdAt": createdAt,
      "creator": creator,
        ------------------
        
        
    };
  }
}

 

    _repository = ref.read(videosRepo);
  }

----------------------
//(26.2)
  //Future<void> uploadVideo(File video) async {
  Future<void> uploadVideo(File video, BuildContext context) async {
  ----------------------
  
  
    final user = ref.read(authRepo).user;
    //state = const AsyncValue.loading();
    //state = await AsyncValue.guard(() async {
      //await _repository.uploadVideoFile(
        //video,
        //user!.uid,
      //);
    //});
    
    ----------------------
    //(26.2)
    final userProfile = ref.read(usersProvider).value;
    if (userProfile != null) {
      state = const AsyncValue.loading();
      state = await AsyncValue.guard(() async {
        final task = await _repository.uploadVideoFile(
          video,
          user!.uid,
        );
        if (task.metadata != null) {
        //디비에 내용 저장
          await _repository.saveVideo(
            VideoModel(
              title: "From Flutter!",
              description: "Hell yeah!",
              //firefuntion에서 생성되는 url처리
              fileUrl: await task.ref.getDownloadURL(),
              thumbnailUrl: "",
              creatorUid: user.uid,
              likes: 0,
              comments: 0,
              createdAt: DateTime.now().millisecondsSinceEpoch,
              creator: userProfile.name,
            ),
          );
          context.pushReplacement("/home");
        }
      });
    }
  }
}
----------------------

----------------------
//(26.2)
final uploadVideoProvider = AsyncNotifierProvider<UploadVideoViewModel, void>(
  () => UploadVideoViewModel(),
);
----------------------

    return fileRef.putFile(video);
  }

----------------
//(26.2)
  // create a video document
  Future<void> saveVideo(VideoModel data) async {
    await _db.collection("videos").add(data.toJson());
  }
}
----------------


final videosRepo = Provider((ref) => VideosRepository());

  Future<void> uploadVideo() async {
    state = const AsyncValue.loading();
    await Future.delayed(const Duration(seconds: 2));
    -------------------
    //(26.2) riverpod예제 잔재 삭제(지금만)
    //final newVideo = VideoModel(title: "${DateTime.now()}");
    //_list = [..._list, newVideo];

    _list = [..._list];
        -------------------
    state = AsyncValue.data(_list);

 

  }

  void _onUploadPressed() async {
  -------------------
  //(26.2)
    //ref.read(timelineProvider.notifier).uploadVideo();
    ref.read(uploadVideoProvider.notifier).uploadVideo(
          File(widget.video.path),
          context,
        );
          -------------------
  }

  @override



IconButton(
  -------------------
  //(26.2)
            //onPressed: ref.watch(timelineProvider).isLoading
            onPressed: ref.watch(uploadVideoProvider).isLoading
              -------------------
              
              
                ? () {}
                : _onUploadPressed,
                
                
                  -------------------
                    //(26.2)
            //icon: ref.watch(timelineProvider).isLoading
            icon: ref.watch(uploadVideoProvider).isLoading
              -------------------
                ? const CircularProgressIndicator()
                : const FaIcon(FontAwesomeIcons.cloudArrowUp),
          )

firestore에서 videos collection에 새 documenet가 있는지 확인하고

새 도큐멘트가 추가되면 이 파일, 이 영상을 가지고,

썸네일을 추출하고 난 다음 썸네일을 새로운 thumbnails라는 폴더에 넣고

그 URL을 thumbnailUrl: ’’ 에 넣는다.

 

26.3 Cloud Functions

overwrite 하자

 

그다음 node.js설치하자

 

TS에 다음과 같이 입력

 

storage등 권한 에러가 나오면 여기서 처리하기

 

 

firebase deploy --only functions

 

 

 

비디오를 업로드하면 hello funtion이 보이게 된다.

 

26.4 ffmpeg

노드JS에서 제공하고 있는 서버로 임의로 커스텀할 수 없는 서버에서 개발 진행한다.

 

 

https://stackoverflow.com/questions/42773497/can-you-call-out-to-ffmpeg-in-a-firebase-cloud-function firebase functions가 더 이상 ffmpeg를 기본적으로 지원하지 않는다고 함. 따라서 ffmpeg가 기본적으로 설치된 버전

을 사용하고 싶다면 우분투 v18.04를 사용하고 있는지 확인해야함 . package.json에서 engines필드에 node 버전을 16으로 낮추면됨

https://nodejs.org/en/download

여기서도 16으로 다운 받아야할 듯

 

 

 

https://www.npmjs.com/package/child-process-promise

그 다음 여기서 코드 등록해야 된다.

 

video.fileUrl부터 의미는

사용자가 업로드한 영상을 입력으로 가져와서,

1초 시간대로 이동해서,

첫번째 프레임을 가져와

영상 필터를 추가함(scale=150:-1 라는 화질 낮은)

마지막으로 결과물인 이미지를 어디에 저장할 건지 쓴다.

중요: 구글클라우드에서 코드가 돌아가는 동안 임시 파일 저장소에 접근가능(코드가 끝나면 접근 불가하니 계속 조회해야할 필요가 있는 걸 거기 넣으면 안됨)

 

중요2. 이 두 경로는 무조건 똑같아야함(upload함수가 진짜로 tmp폴더로 가서 이 파일 찾기 떄문)

 

firebase deploy --only functions

 

 

 

26.5 publicUrl

Nosql의 처리과정 코딩

users에 가서,

영상을 만든 사용자를 찾아서,

그 사용자 내에 collection을 만들어서,

만들어진 영상의 id를 가지고 document를 만들어서

thumbnailUrl이랑 videoId만 놓았다.

 

다시 비디오를 업로드해보고 확인해보면

 

documentID videoID같은 거 확인 가능

 

 

 

 

/**
 * Import function triggers from their respective submodules:
 *
 * import {onCall} from "firebase-functions/v2/https";
 * import {onDocumentWritten} from "firebase-functions/v2/firestore";
 *
 * See a full list of supported triggers at https://firebase.google.com/docs/functions
 */

// import {onRequest} from "firebase-functions/v2/https";
// import * as logger from "firebase-functions/logger";

import * as functions from "firebase-functions";
import * as admin from "firebase-admin";

// Start writing functions
// https://firebase.google.com/docs/functions/typescript

// export const helloWorld = onRequest((request, response) => {
//   logger.info("Hello logs!", {structuredData: true});
//   response.send("Hello from Firebase!");
// });

admin.initializeApp();

//비디오 업로드하기위한 함수
export const onVideoCreated = functions.firestore
  .document("videos/{videoId}")
  .onCreate(async (snapshot, context) => {
    // await snapshot.ref.update({ hello: "from functions" });
    //(26.4)
    const spawn = require("child-process-promise").spawn;
    // 갓 디비에 업로드된 데이터를 줌
    const video = snapshot.data();
    await spawn("ffmpeg", [
      "-i",
      video.fileUrl,
      "-ss",
      "00:00:01.000",
      "-vframes",
      "1",
      "-vf",
      "scale=150:-1",
      `/tmp/${snapshot.id}.jpg`,
    ]);
    const storage = admin.storage();
    //(26.5)
    // await storage.bucket().upload(`/tmp/${snapshot.id}.jpg`, {
      const [file, _] = await storage.bucket().upload(`/tmp/${snapshot.id}.jpg`, {
      destination: `thumbnails/${snapshot.id}.jpg`,
    });
    await file.makePublic();
    await snapshot.ref.update({ thumbnailUrl: file.publicUrl() });
    const db = admin.firestore();
    await db
      .collection("users")
      .doc(video.creatorUid)
      .collection("videos")
      .doc(snapshot.id)
      .set({
        thumbnailUrl: file.publicUrl(),
        videoId: snapshot.id,
      });
  });

 

 

github에 올릴 때 여기 apikey 조심하기

728x90
반응형
Comments