フリーランス 技術調査ブログ

フリーランス/エンジニア Ruby Python Nodejs Vuejs React Dockerなどの調査技術調査の備忘録

shopify アプリ開発の調査メモ

新しいアプリを作成する

  • プロジェクト用アプリの開発
$ mkdir shopify_dev
$ cd shopify_dev/
  • shopifyのパッケージをインストール
npm init @shopify/app@latest
  • shopifyプロジェクトの初期設定。プロジェクト名と、どの言語を利用するのか問われる。言語は、node,php,rubyから選択可能。 今回はnodeを指定する
✔ Your app's name? · example-mo-dev
✔ Which template would you like to use? · node
  • shopifyアプリの起動
cd example-mo-dev/
npm run dev

✔ Create this project as a new app on Shopify? · Yes, create it as a new app
✔ App Name · example-mo-dev
※パートナーアカウントの認証確認がこのタイミングで入ります。
✔ Which development store would you like to use to view your project? · <検証に利用するショップ名を指定する>
````

- 開発にはngrokが必要でngrokのアカウント作成とngrokのトークンが必要である
````
To make your local code accessible to your dev store, you need to use a Shopify-trusted tunneling service called ngrok. To sign up and get an auth token: https://dashboard.ngrok.com/get-started/your-authtoken (​https://dashboard.ngrok.com/get-started/your-authtoken​)
````

- ngrokの認証キーを取得できたら、コピーペーストで入力する
[f:id:PX-WING:20221108223028p:plain]


- アプリ開発用のURLを自動で生成するかと問われるため、「Always by default」を選択する

````
Have Shopify automatically update your app's URL in order to create a preview experience?

> Always by default
  Yes, this time
  No, not now
  Never, don't ask again

※※※上記の翻訳は下記となります。※※※

Shopifyは、プレビュー体験を作成するために、アプリのURLを自動的に更新していますか?

> デフォルトで常に
  はい、今回
  いいえ、今すぐではありません
  二度と聞かない
  • ngrokで生成されたURLが表示されるため、そちらをクリックする

  • shopへのインストール画面が表示されるので、画面右上の「アプリをインストール」ボタンをクリックする

  • shopへ開発用アプリをインストールすることが出来た。

AWS Elastic BeanstalkでLaravelを動作させる

はじめに

  • EBでLaravelを動作させる機会があったので、下記にまとめました。

ElasticBeanstalk アプリケーション作成

  • 「Create Application」ボタンをクリックする

  • 「アプリケーション名」を設定する

  • PHP、ファイルアップロードを選択する

  • アプリケーションの作成ボタンをクリックする

  • アプリケーションの作成ボタンをクリックすると数分かかるので、しばらく待つ

  • ドキュメントのルートに「/public」を指定する

アップロード用のファイルを作成する

  • アプリケーションフォルダのルート直下で下記のコマンドを実行する
zip ../laravel-default.zip -r * .[^.]* -x "vendor/*" "docker/*"

デプロイ時にマイグレーションを走らせたい場合

  • .platform/hooks/postdeploy/migrate.shファイルを作成し下記の内容を記述する。
sudo chmod -R 777 storage/
sudo chmod -R 777 bootstrap/cache/
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan migrate

Nginxの設定を変更したい場合

  • .platform/nginx/conf.d/elasticbeanstalk/proxy.confファイルを作成し下記の記述を追加する。
client_max_body_size 5000M;

PHP.iniの設定を変更したい場合

  • .ebextensions/change_upload_size.configファイルを作成し下記の記述を追加する
files:
    "/etc/php.d/99uploadsize.ini":
        mode: "000644"
        owner: root
        group: root
        content: |
            upload_max_filesize = 1024M
            post_max_size = 1024M
commands:
    remove_old_ini:
        command: "rm -f /etc/php.d/99uploadsize.ini.bak"

S3に独自ドメインを設定する

はじめに

  • S3にSSL通信でアクセスできるための設定を下記のまとめてみました。

AWS Certificate Managerの設定

  • SSL証明書の定義

  • 証明書をリクエスト「パブリック証明書をリクエスト」を選択し、「次へ」ボタンをクリックする

  • ドメイン名にSSLを設定したいドメイン名を指定する。検証方法の選択は、デフォルトの「DNS検証」を選択したまま、「リクエスト」ボタンをクリックする

  • 証明書の一覧画面に遷移しリクエストした証明書が登録されていることを確認する。ステータスは「保留中の検証」の状態であることを確認する

CloudFrontの設定

  • ディストリビューションを作成」ボタンをクリックする

  • オリジンドメインに対象となる「S3」のバケットを選択する。S3のバケットアクセスは「Origin access control settings (recommended)」を選択する

  • デフォルトのキャッシュビヘイビアはデフォルトの状態にする

  • ACLの指定があるが値は指定しなくてもよいがIAMのポリシーに「WAF2」の権限の指定が必要。代替ドメイン名とカスタムSSL証明書を設定する

Route53の設定

AWS Elemental MediaConvertの設定

はじめに

AWS Elemental MediaConvertを利用する機械があったので、設定する手順を下記に記載しました。

ジョブテンプレートの作成

- 一般設定を下記のように任意の値を入力する - 入力は何も設定せず、デフォルトのまま

  • 出力の追加ボタンをクリックし「Apple HLS」を選択する

  • 変換後のファイルをアップロードするS3を指定する

  • 「名前修飾子」に「_hls」を追加する

  • 画面右上の赤枠のメニューをクリックし設定画面に遷移し、ビデオコーデックなどの設定を行い、作成ボタンをクリックする
ビデオコーデック:MPEG-4 AVC (H.264)
解像度:1280 x 720
フレームレート:30fps(1秒間に30回更新されるという意味です。)
ビットレート:5Mbps(1秒間の転送データ量です。)
ピクセルアスペクト比:16:9

ロール作成

  • ロール作成ボタンをクリックし「他の AWS のサービスのユースケース」に「MediaConvert」を選択し「次へ」ボタンをクリックする

- 何も変更せずに「次へ」ボタンをクリックする

  • 任意のロール名を指定して「ロール作成」ボタンをクリックする

S3のオブジェクト所有者の設定

プログラムで利用するJob作成用Jsonを出力する

  • 出力されたJSONファイルで下記の部分だけ抽出する
    "OutputGroups": [
      {
        "Name": "Apple HLS",
        "Outputs": [
          {
            "ContainerSettings": {
              "Container": "M3U8",
              "M3u8Settings": {}
            },
            "VideoDescription": {
              "Width": 1280,
              "Height": 720,
              "CodecSettings": {
                "Codec": "H_264",
                "H264Settings": {
                  "ParNumerator": 16,
                  "FramerateDenominator": 1,
                  "MaxBitrate": 50000,
                  "ParDenominator": 9,
                  "FramerateControl": "SPECIFIED",
                  "RateControlMode": "QVBR",
                  "FramerateNumerator": 30,
                  "SceneChangeDetect": "TRANSITION_DETECTION"
                }
              }
            },
            "AudioDescriptions": [
              {
                "AudioSourceName": "Audio Selector 1",
                "CodecSettings": {
                  "Codec": "AAC",
                  "AacSettings": {
                    "Bitrate": 96000,
                    "CodingMode": "CODING_MODE_2_0",
                    "SampleRate": 48000
                  }
                }
              }
            ],
            "OutputSettings": {
              "HlsSettings": {}
            },
            "NameModifier": "_hls"
          }
        ],
        "OutputGroupSettings": {
          "Type": "HLS_GROUP_SETTINGS",
          "HlsGroupSettings": {
            "SegmentLength": 10,
            "Destination": "s3://<your bucket>/",
            "MinSegmentLength": 0
          }
        }
      }
    ],

次回

  • 上記で設定が完了したので、次はプログラムからAWS Elemental MediaConvertをプログラムから読んでみたいと思います。

Reactで動画再生する

はじめに

  • お仕事でReactで動画再生する方法を検討する
  • 調査したいこと、動画開始、終了イベント及び再生時間の取得ができるか

該当プラグイン

調べたところ下記の2つのプラグインがあることを知りstar数の数の多いreact-playerを検証してみる。

  • video-react  startの数:2.3k

https://github.com/video-react/video-react

  • react-player startの数:6.7k

https://github.com/cookpete/react-player https://github.com/cookpete/react-player/issues/1474

サンプルコード

import React, { useState, useEffect, useRef } from 'react'
//import ReactPlayer from 'react-player/lazy'
import dynamic from "next/dynamic";
const ReactPlayer = dynamic(() => import("react-player/lazy"), { ssr: false });
// https://video-ac.com/video/4648
// https://www.pexels.com/ja-jp/search/videos/%E9%A2%A8%E6%99%AF/?size=small&orientation=landscape
/*
https://stackoverflow.com/questions/68374975/get-total-played-time-with-react-player
*/
const Movie = () => {
  const [hasWindow, setHasWindow] = useState(true);
  const [started, setStarted] = useState<number>(0);
  const [ended, setEnded] = useState(0);
  const [isDisplay, setIsDisplay] = useState(false);
  useEffect(() => {
    if (typeof window !== "undefined") {
      setHasWindow(true);
    }
  }, []);

  const start = () => {
    const start = new Date()
    setStarted(start.getTime());
  };
  const end = () => {
    const end = new Date()
    setEnded(end.getTime());
    setIsDisplay(true);
  };    
  return (
    <>
    <div className="static">
      <div className="flex justify-center mx-auto bg-white shadow-lg shadow-none rounded m-6 p-6">
          {hasWindow && 
          <ReactPlayer 
          url={'<動画のURLがはいります>'} 
          controls={true}
          muted={true}
          onStart={start}
          onEnded={end}
          volume={1}
        />}
      </div>
      <div className="flex justify-center mx-auto bg-white shadow-lg shadow-none rounded m-6 p-6">
          {(ended - started > 0) ? `${(ended - started) / 1000}秒` : started === 0 ?  '再生前' : `再生中` }
      </div>
      <div className="absolute top-2 left-96">
        <div style={{display: isDisplay ? "block": "none"}} id="defaultModal" tabIndex={-1} className="overflow-y-auto overflow-x-hidden inset-0 z-50 top-10 flex justify-center items-center" aria-modal="true" role="dialog">
          <div className="relative p-4 w-full max-w-2xl h-full md:h-auto">
              <div className="relative bg-white rounded-lg shadow dark:bg-gray-700">
                  <div className="flex justify-between items-start p-4 rounded-t border-b dark:border-gray-600">
                      <h3 className="text-xl font-semibold text-gray-900 dark:text-white">
                          視聴完了
                      </h3>
                      <button type="button" className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-toggle="defaultModal">
                          <svg aria-hidden="true" className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd"></path></svg>
                          <span className="sr-only">Close modal</span>
                      </button>
                  </div>
                  <div className="p-6 space-y-6">
                    <div className="flex items-center pl-4 rounded border border-gray-200 dark:border-gray-700">
                        <input id="bordered-radio-1" type="radio" value="" name="bordered-radio" className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
                        <label htmlFor="bordered-radio-1" className="py-4 ml-2 w-full text-sm font-medium text-gray-900 dark:text-gray-300">良かった</label>
                    </div>
                    <div className="flex items-center pl-4 rounded border border-gray-200 dark:border-gray-700">
                        <input checked id="bordered-radio-2" type="radio" value="" name="bordered-radio" className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
                        <label htmlFor="bordered-radio-2" className="py-4 ml-2 w-full text-sm font-medium text-gray-900 dark:text-gray-300">普通</label>
                    </div>
                    <div className="flex items-center pl-4 rounded border border-gray-200 dark:border-gray-700">
                        <input checked id="bordered-radio-2" type="radio" value="" name="bordered-radio" className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
                        <label htmlFor="bordered-radio-2" className="py-4 ml-2 w-full text-sm font-medium text-gray-900 dark:text-gray-300">悪かった</label>
                    </div>
                  </div>
                  <div className="flex items-center p-6 space-x-2 rounded-b border-t border-gray-200 dark:border-gray-600">
                      <button data-modal-toggle="defaultModal" type="button" className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" onClick={() => {setIsDisplay(false)}}>設定</button>
                  </div>
              </div>
          </div>
        </div>
      </div>
      </div>      
    </>
  )  
}

export default Movie

画面

  • 再生開始、終了イベントの取得はできたが、再生時間の取得がうまくできなかった。

結論

  • videojsだと再生開始・終了イベントを取得でき、且つ再生している時間を取得できるので、videojsがよさそうという結論になった
https://codesandbox.io/s/react-videojs-currenttime-zvlbn?file=/src/VideoPlayer.js:1070-1081
https://videojs.com/guides/react/
https://videojs.com/advanced?video=disneys-oceans

Flutterでタイマーアプリをつくる

はじめに

  • 下記の動画を参考に自分のアレンジを入れてタイマーを作成してみました。 www.youtube.com

サンプルコード

import 'dart:ui';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:iterative_memory/widget/button_widget.dart';

class TimerPage extends StatefulWidget {
  @override
  _TimerPageState createState() => _TimerPageState();

}

class _TimerPageState extends State<TimerPage> {
  final player = AudioPlayer();

  //static const defaultSeconds = 10;
  int maxSeconds = 10;
  int seconds = 10;
  Timer? timer;

  void startTimer({bool reset = true}) {
    if (reset) {
      resetTimer();
    }
    // seconds: 1
    timer = Timer.periodic(Duration(seconds: 1), (_){
      if (seconds > 0) {
        setState(() => seconds--);
      } else {
        stopTimer(reset: false);
        seconds = maxSeconds;
      }
    });
  }

  void resetTimer() => setState(() => seconds = maxSeconds);

  void stopTimer({bool reset = true}) {
    if (reset) {
      resetTimer();
    }
    setState(() => timer?.cancel());
  }

  @override
  Widget build(BuildContext context) => Scaffold(
      body: Container(
          width: double.infinity,
          color: Colors.deepPurpleAccent,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              buildTimer(),
              const SizedBox(height: 80),
              buildButtons()
            ],
          )
      ));

  Widget buildButtons() {
    final isRunning = timer == null ? false : timer!.isActive;
    final isCompleted = seconds == maxSeconds || seconds == 0;
    final timeController = TextEditingController();

    return isRunning || !isCompleted ? Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ButtonWidget(
              text: isRunning ? '停止' : '再開',
              onClicked: () {
                if (isRunning) {
                  stopTimer(reset: false);
                } else {
                  startTimer(reset: false);
                }
              }),
          const SizedBox(width: 12),
          ButtonWidget(text: '初めから', onClicked: () {
            resetTimer();
          })
        ]
    ) : Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        TextField(
          decoration: InputDecoration(
              fillColor: Colors.white,
              filled: true,
              border: OutlineInputBorder(),
              hintText: '秒数を入力してください'
          ),
          keyboardType: TextInputType.number,
          autofocus: true,
          controller: timeController,
        ),
        ButtonWidget(
          text: 'スタート',
          color: Colors.black,
          backgroundColor: Colors.white,
          onClicked: () {
            maxSeconds =  int.parse(timeController.text);
            startTimer();
          },
        ),
      ]
    );
  }

  Widget buildTimer() => SizedBox(
      width: 200,
      height: 200,
      child: Stack(
        fit: StackFit.expand,
        children: [
          CircularProgressIndicator(
            value: 1 - seconds / maxSeconds,
            valueColor: AlwaysStoppedAnimation(Colors.white),
            strokeWidth: 12,
            backgroundColor: Colors.greenAccent,
          ),
          Center(child: buildTime(),)
        ],
      )
  );

  Widget buildTime() {
    return Text(
        '$seconds',
        style: TextStyle(
            fontWeight: FontWeight.bold,
            color: Colors.white,
            fontSize: 80
        )
    );
  }
}
  • Buttonを共通化するためにcomponent化
import 'package:flutter/material.dart';

class ButtonWidget extends StatelessWidget {
  final String text;
  final VoidCallback onClicked;

  var backgroundColor;
  var color;

  ButtonWidget({
    Key? key,
    required this.text,
    this.color = Colors.white,
    required this.onClicked,
    this.backgroundColor = Colors.black,
  }) : super(key: key) {

  }

  @override
  Widget build(BuildContext context) => ElevatedButton(
    style: ElevatedButton.styleFrom(
      primary: backgroundColor,
      padding: EdgeInsets.symmetric(horizontal: 32,vertical: 16)
    ),
    onPressed: onClicked,
    child: Text(
      text,
      style: TextStyle(fontSize: 20, color: color)
    ),
  );

}

画面イメージ

  • 秒数を入れてスタートボタンをクリックするとタイマーが開始します。

  • タイマーが始まり終わると、入力画面に戻るようになっております。

Google reCAPTCHAの設定/ amplifyのauthの設定 

Google reCAPTCHA

reCAPTCHA設定

https://www.google.com/recaptcha/intro/v3.html

画面上部の中央にある「Admin console」をクリックします。

amplifyに認証設定する

amplify add authのコマンドを実行し下記の選択を行う

$ amplify add auth
Using service: Cognito, provided by: awscloudformation
 Do you want to use the default authentication and security configuration? [Default configuration]

 How do you want users to be able to sign in? [Email]

 Do you want to configure advanced settings? [Yes, I want to make some additional changes.]

※default以外に登録に必要な属性は何ですか?→誕生日、性別などを追加指定しました。
 What attributes are required for signing up? [Birthdate (This attribute is not supported by Login With Amazon, Signinwithapple.), Email, Gender (This attribute is not s
upported by Login With Amazon, Signinwithapple.), Nickname (This attribute is not supported by Facebook, Google, Login With Amazon, Signinwithapple.), Updated At (This
attribute is not supported by Google, Login With Amazon, Signinwithapple.)]

※ 以下の機能のいずれかを有効にしますか? →「Google reCaptcha」を追加する
 Do you want to enable any of the following capabilities? [Add Google reCaptcha Challenge]

Do you want to edit your captcha-define-challenge function now? [Yes]
/<app_root>/amplify/backend/function/winlogic1d56dd94DefineAuthChallenge/src/captcha-define-challenge.js

? Do you want to edit your captcha-create-challenge function now? No
✔ Enter the Google reCaptcha secret key: ·[google reCAPTCHAで取得したシークレットキーを指定する]

? Do you want to edit your captcha-verify function now? [Yes]
/<app_root>/amplify/backend/function/winlogic1d56dd94VerifyAuthChallengeResponse/src/captcha-verify.js

amplify pushする

  • ローカルバックエンドリソースを構築し、クラウドでプロビジョニングします

amplify サインアップサンプル

  • ボタンを押すと、固定のユーザーとなりますが、amplifyのsignUpメソッドを実行するサンプルとなります。
import { Amplify, Auth } from 'aws-amplify';
import awsExports from '../src/aws-exports';
Amplify.configure(awsExports);

import styles from '../styles/Home.module.css';

async function signUp() {
  try {
   // 更新日付を取得
    const date = new Date();
    const unixtimeUpdatedAt = date.getTime() ;

    const { user } = await Auth.signUp({
            username: "<your email>",
            password: '<your password>',
            attributes: {
              email: "<your email>",
              gender: 'm',
              nickname: 'hogehoge',
              birthdate: '<your birthdate>',
              updated_at: unixtimeUpdatedAt.toString()
            }
        });
        console.log(user);
    } catch (error) {
        console.log('error signing up:', error);
    }
}

export default function signingUp() {
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <button onClick={signUp} className="shadow bg-teal-400 hover:bg-teal-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded" type="button">
          Signing Up!!
        </button>
      </main>
    </div>
  );
}

次回

  • フォームからユーザーを登録できるようにする。またGoogle reCAPTCHAを導入にチャレンジジョイしてみます。