[{"data":1,"prerenderedAt":9537},["ShallowReactive",2],{"category-devstack-2":3},{"count":4,"content":5},43,[6,944,2407,3228,5205,6290,6365,7695,8743,9058],{"id":7,"title":8,"body":9,"category":929,"createdAt":931,"description":932,"extension":933,"index":934,"meta":935,"navigation":936,"path":937,"publish":936,"seo":938,"series":934,"seriesTitle":934,"stem":939,"tag":940,"thumbnail":942,"updatedAt":931,"__hash__":943},"articles\u002Farticles\u002Fs3-clioudfront-website.md","AWS S3を使ってSSL&独自ドメインの静的ブログをホストする",{"type":10,"value":11,"toc":911},"minimark",[12,16,35,38,45,49,59,66,70,73,78,86,89,92,95,98,118,121,124,127,130,133,136,348,364,378,384,387,390,394,397,415,418,423,432,435,438,441,444,447,450,453,456,459,462,465,468,477,481,484,487,490,493,496,499,502,505,508,511,515,524,531,534,542,545,548,551,559,562,572,579,582,585,588,591,596,602,627,630,643,652,656,663,667,670,676,679,682,685,688,691,694,697,700,703,706,727,730,734,751,779,801,807,815,818,822,836,894,897,900,904,907],[13,14,15],"p",{},"こんにちはjunです。会社で静的書き出ししたコンテンツをs3でホストし、cloudfrontを用いてwebサーバーの様にブログ内容をリクエストできる様な構成を作成しました。せっかくなので自分のブログも同じ様にやってみようと思い、復習兼ねて記事にしようと思います。今回は以下のことを説明します。",[17,18,19,23,26,29,32],"ul",{},[20,21,22],"li",{},"S3の設定",[20,24,25],{},"cloudfrontの設定",[20,27,28],{},"独自ドメインの設定（DNSはお名前ドットコム）",[20,30,31],{},"IAMでのデプロイユーザーの作成",[20,33,34],{},"nuxt generate で転送する方法",[13,36,37],{},"私のこのブログはnuxt content を用いてnuxt generate をして静的書き出したものをサーバに転送しています。今まではレンタルサーバーの一部ディレクトリを使用していましたが、AWSの勉強ややってみたい機能をつけやすくするためにS3へ引っ越しました。それでは早速やっていきましょう。",[39,40,44],"div",{"className":41},[42,43],"alert","alert-danger","\nこの記事が説明している内容は2021年10月時点の情報です。\n",[46,47,48],"h2",{"id":48},"参考資料",[13,50,51,52],{},"\b",[53,54,58],"a",{"href":55,"rel":56},"https:\u002F\u002Faws.amazon.com\u002Fjp\u002Fpremiumsupport\u002Fknowledge-center\u002Fcloudfront-serve-static-website#Using_a_website_endpoint_as_the_origin.2C_with_anonymous_.28public.29_access_allowed",[57],"nofollow","CloudFront を使用して、Amazon S3 でホストされた静的ウェブサイトを公開するにはどうすればよいですか?",[13,60,61],{},[53,62,65],{"href":63,"rel":64},"https:\u002F\u002Fdocs.aws.amazon.com\u002Fja_jp\u002FRoute53\u002Flatest\u002FDeveloperGuide\u002Frouting-to-cloudfront-distribution.html",[57],"ドメイン名を使用したトラフィックの Amazon CloudFront ディストリビューションへのルーティング",[46,67,69],{"id":68},"s3でデプロイ用のバケットを作成する","S3でデプロイ用のバケットを作成する。",[13,71,72],{},"では最初にまずホスティング元であるバケットを作成します。",[74,75],"image-render",{":src":76,":width":77},"'s3-clioudfront-website\u002Fsch-iam-4.png'","'100%'",[13,79,80,81,85],{},"バケットの名前は ",[82,83,84],"strong",{},"ホストするドメインの名前と同じ"," になるようにします。私の場合はバケット名が「jun-app.com」です。",[74,87],{":src":88,":width":77},"'s3-clioudfront-website\u002Fsch-s3-1.png'",[13,90,91],{},"設定した後にバケット一覧から選択し、プロパティを選びます。プロパティの一番下までスクロールすると「静的ウェブサイトホスティング」とあるのでここを編集します。",[74,93],{":src":94,":width":77},"'s3-clioudfront-website\u002Fsch-s3-2.png'",[74,96],{":src":97,":width":77},"'s3-clioudfront-website\u002Fsch-s3-3.png'",[13,99,100,101,105,106,109,110,113,114,117],{},"無効から有効に変更し、インデックスドキュメントに",[102,103,104],"code",{},"index.html","を入力します。こうすると",[102,107,108],{},"https:\u002F\u002Fjun-app.com\u002Fdir\u002F","みたいにパスだけであっても",[102,111,112],{},"https:\u002F\u002Fjun-app.com\u002Fdir\u002Findex.html","に接続してくれます。また、エラードキュメントを指定しておくことで404リクエストの際のフォールバックとして利用できます。私の場合はルート直下の ",[102,115,116],{},"404.html","を指定しています。",[74,119],{":src":120,":width":77},"'s3-clioudfront-website\u002Fsch-s3-4.png'",[13,122,123],{},"編集後もういちど静的ウェブサイトホスティングの設定までいくと、一応URLが提供されますがアクセスしても「403 Forbidden」となります。これはバケットに対してパブリックアクセスがないからです。バケット作成時、デフォルトでは全てのパブリックアクセスが制限されていたので、その制限を外してあげます。バケットの画面から「アクセス許可」を選択し、「ブロックパブリックアクセス (バケット設定)」の編集をします。（ここを見ると「パブリックアクセスをすべて ブロック」が有効になっている）",[74,125],{":src":126,":width":77},"'s3-clioudfront-website\u002Fsch-s3-5.png'",[13,128,129],{},"編集の画面で「パブリックアクセスをすべて ブロック」のチェックを外して、全てのブロックを外します。",[74,131],{":src":132,":width":77},"'s3-clioudfront-website\u002Fsch-s3-6.png'",[13,134,135],{},"まだ終わりではありません。先ほどのはバケット単位の設定であり、アップロードされるオブジェクト（静的ファイル）には公開される設定がされません。毎回オブジェクトに対してパブリックアクセスを与えるのは面倒なので、「アクセス許可」の画面にあるバケットポリシーを以下のように編集します。",[137,138,143],"pre",{"className":139,"code":140,"language":141,"meta":142,"style":142},"language-JSON shiki shiki-themes material-theme-ocean","{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"PublicReadGetObject\",\n            \"Effect\": \"Allow\",\n            \"Principal\": \"*\",\n            \"Action\": [\n                \"s3:GetObject\"\n            ],\n            \"Resource\": [\n                \"arn:aws:s3:::your-buget-name\u002F*\"\n            ]\n        }\n    ]\n}\n","JSON","",[102,144,145,154,182,197,203,226,247,268,282,294,300,314,324,330,336,342],{"__ignoreMap":142},[146,147,150],"span",{"class":148,"line":149},"line",1,[146,151,153],{"class":152},"sAklC","{\n",[146,155,157,160,164,167,170,173,177,179],{"class":148,"line":156},2,[146,158,159],{"class":152},"    \"",[146,161,163],{"class":162},"sJ14y","Version",[146,165,166],{"class":152},"\"",[146,168,169],{"class":152},":",[146,171,172],{"class":152}," \"",[146,174,176],{"class":175},"sfyAc","2012-10-17",[146,178,166],{"class":152},[146,180,181],{"class":152},",\n",[146,183,185,187,190,192,194],{"class":148,"line":184},3,[146,186,159],{"class":152},[146,188,189],{"class":162},"Statement",[146,191,166],{"class":152},[146,193,169],{"class":152},[146,195,196],{"class":152}," [\n",[146,198,200],{"class":148,"line":199},4,[146,201,202],{"class":152},"        {\n",[146,204,206,209,213,215,217,219,222,224],{"class":148,"line":205},5,[146,207,208],{"class":152},"            \"",[146,210,212],{"class":211},"s5Dmg","Sid",[146,214,166],{"class":152},[146,216,169],{"class":152},[146,218,172],{"class":152},[146,220,221],{"class":175},"PublicReadGetObject",[146,223,166],{"class":152},[146,225,181],{"class":152},[146,227,229,231,234,236,238,240,243,245],{"class":148,"line":228},6,[146,230,208],{"class":152},[146,232,233],{"class":211},"Effect",[146,235,166],{"class":152},[146,237,169],{"class":152},[146,239,172],{"class":152},[146,241,242],{"class":175},"Allow",[146,244,166],{"class":152},[146,246,181],{"class":152},[146,248,250,252,255,257,259,261,264,266],{"class":148,"line":249},7,[146,251,208],{"class":152},[146,253,254],{"class":211},"Principal",[146,256,166],{"class":152},[146,258,169],{"class":152},[146,260,172],{"class":152},[146,262,263],{"class":175},"*",[146,265,166],{"class":152},[146,267,181],{"class":152},[146,269,271,273,276,278,280],{"class":148,"line":270},8,[146,272,208],{"class":152},[146,274,275],{"class":211},"Action",[146,277,166],{"class":152},[146,279,169],{"class":152},[146,281,196],{"class":152},[146,283,285,288,291],{"class":148,"line":284},9,[146,286,287],{"class":152},"                \"",[146,289,290],{"class":175},"s3:GetObject",[146,292,293],{"class":152},"\"\n",[146,295,297],{"class":148,"line":296},10,[146,298,299],{"class":152},"            ],\n",[146,301,303,305,308,310,312],{"class":148,"line":302},11,[146,304,208],{"class":152},[146,306,307],{"class":211},"Resource",[146,309,166],{"class":152},[146,311,169],{"class":152},[146,313,196],{"class":152},[146,315,317,319,322],{"class":148,"line":316},12,[146,318,287],{"class":152},[146,320,321],{"class":175},"arn:aws:s3:::your-buget-name\u002F*",[146,323,293],{"class":152},[146,325,327],{"class":148,"line":326},13,[146,328,329],{"class":152},"            ]\n",[146,331,333],{"class":148,"line":332},14,[146,334,335],{"class":152},"        }\n",[146,337,339],{"class":148,"line":338},15,[146,340,341],{"class":152},"    ]\n",[146,343,345],{"class":148,"line":344},16,[146,346,347],{"class":152},"}\n",[13,349,350,351,353,354,356,357,359,360,363],{},"上記のJSONは",[102,352,254],{},"はこのポリシーを付与するユーザーです。",[102,355,263],{},"としてゲストを含む全てのユーザーに",[102,358,290],{},"のアクション、つまりファイルの読み取りを",[102,361,362],{},"\"arn:aws:s3:::your-buget-name\u002F*\"","のバケットオブジェクトに許可するという意味です。",[13,365,366,369,370,373,374,377],{},[102,367,368],{},"your-buget-name","にはあなたが設定したバケット名を入力します。そしてそのバケット配下の全オブジェクトに適用するため",[102,371,372],{},"your-buget-name\u002F*","とディレクトリのように指定します。",[102,375,376],{},"\"arn:aws:s3:::your-buget-name","という記述（Amazon リソースネーム）はバケットのプロパティで取得できます。",[13,379,380,381,383],{},"こうすればこのバケットないのファイルは公開されます。試しに",[102,382,104],{},"をアップロードして、提供されたオブジェクト URLにアクセスすると確かに見れます。",[74,385],{":src":386,":width":77},"'s3-clioudfront-website\u002Fsch-s3-7.png'",[13,388,389],{},"これでS3側の設定が完了しました。ただしドメインはS3の初期状態のままです。独自ドメインを割り当てとSSL化を行います。",[46,391,393],{"id":392},"sslと独自ドメインを割り当てる","SSLと独自ドメインを割り当てる",[13,395,396],{},"SSL化した独自ドメインをS3に接続させるためにはcloudfrontの力が必要です。また今回使用するドメイン「jun-app.com」はお名前ドットコムで取得しているので、CNAMEを加えるなどの設定が必要です。クライアントがコンテンツを取得する流れは以下のようになります。",[398,399,400,403,406,409,412],"ol",{},[20,401,402],{},"クライアントがURLにアクセス",[20,404,405],{},"お名前ドットコムへDNS問い合わせ",[20,407,408],{},"お名前ドットコムはRoute53にNSを移譲しているので、問い合わせはRoute53へ",[20,410,411],{},"Route53 はcloudfrontのIPを返す",[20,413,414],{},"cloudfrontからS3に接続",[13,416,417],{},"といった流れです。",[419,420,422],"h3",{"id":421},"route53と外部dnsの設定","Route53と外部DNSの設定",[13,424,425,426,431],{},"まずはRoute53でホストゾーンというものを作成して、お名前.comからNSサーバーをRoute53へ委任できるようにします。",[53,427,430],{"href":428,"rel":429},"https:\u002F\u002Fconsole.aws.amazon.com\u002Froute53\u002Fv2\u002Fhome#Dashboard",[57],"Route53","に移動して「ホストゾーンの作成」をします。",[74,433],{":src":434,":width":77},"'s3-clioudfront-website\u002Fsch-r53-1.png'",[13,436,437],{},"委任したいドメインを入力します。「パブリックホストゾーン」で大丈夫です。タグは管理上のものなので空欄で大丈夫です。問題なければ「ホストゾーンの作成」をクリックします。",[74,439],{":src":440,":width":77},"'s3-clioudfront-website\u002Fsch-r53-2.png'",[13,442,443],{},"作成後にはこのようにNSレコードとSOAレコードが作成されます。ドメイン自体が外部DNS（お名前.com）にある場合この「NSレコード」を指定することでRoute53に委任します。（ぼやけていますが４つ作成されます。）",[74,445],{":src":446,":width":77},"'s3-clioudfront-website\u002Fsch-r53-3.png'",[13,448,449],{},"このNSレコードをお名前.comの場合は「ネームサーバーの設定」を選択します。",[74,451],{":src":452,":width":77},"'s3-clioudfront-website\u002Fsch-r53-4.png'",[13,454,455],{},"そして「2.ネームサーバーの選択」＞「その他」＞「その他のネームサーバーを使う」にてRoute53で表示された４つのNSレコードをいれて「確認」クリックします。",[74,457],{":src":458,":width":77},"'s3-clioudfront-website\u002Fsch-r53-5.png'",[13,460,461],{},"「確認」の後にはDNSのNSが切り替わり、「jun-app.com」はまずお名前.comへ問い合わせられますが、結局Route53へたらい回しされます。これで「jun-app.com」のAレコードなどをRoute53で制御できるようになりました。",[13,463,464],{},"次はS3とcloudfrontと連携させてSSLを利用できるようにします。",[419,466,467],{"id":467},"独自ドメインの証明書を取得する",[13,469,470,471,476],{},"まずcloudfrontの設定の前に独自ドメイン（jun-app.com）のSSL証明書をAWSで取得しておきます。AWSでは",[53,472,475],{"href":473,"rel":474},"https:\u002F\u002Fap-northeast-1.console.aws.amazon.com\u002Facm\u002Fhome",[57],"AWS Certificate Manager"," で取得できます。ただしここで注意点があります。",[39,478,480],{"className":479},[42,43],"\n以下の画像のようにAWS Certificate Managerでのカスタム証明書(独自ドメインの証明書)をcloudfrontに割り当てる場合、AWS Certificate Managerのリージョンが「米国東部 (バージニア北部) リージョン (us-east-1) 」でないといけません。私は間違って東京リージョンで作成してしまい、証明書がサジェストで出てこずはまりました。\n",[74,482],{":src":483,":width":77},"'s3-clioudfront-website\u002Fsch-cfm-1.png'",[13,485,486],{},"リージョンを「us-east-1」に変更したのち「証明書をリクエスト」をクリックして作成します。",[74,488],{":src":489,":width":77},"'s3-clioudfront-website\u002Fsch-cfm-2.png'",[13,491,492],{},"証明書のタイプは「パブリック証明書をリクエスト」を選択し、ドメイン名などを入力します。そして検証では「DNS検証」を行います。Route53で委任してあれば「DNS検証」ですぐに発行できます。DNS検証はドメインを管理しているDNSにAWS Certificate Managerで発行されたCNAMEを設定することで、ドメインの所有権を確認します。全て入力して「リクエスト」となります。",[74,494],{":src":495,":width":77},"'s3-clioudfront-website\u002Fsch-cfm-3.png'",[13,497,498],{},"リクエスト後には一覧に入力したドメインが現れ、対応するCNAMEが現れます。Route53で管理している場合は「Route53でレコードを作成」のボタンを押すだけで作ってくれます。手動で設定する場合は「CNAMEレコード」を作成して、その値を設定するだけです。",[74,500],{":src":501,":width":77},"'s3-clioudfront-website\u002Fsch-cfm-4.png'",[13,503,504],{},"このようにDNS検証に必要なCNAMEが追加されています。",[74,506],{":src":507,":width":77},"'s3-clioudfront-website\u002Fsch-cfm-5.png'",[13,509,510],{},"数分経つと証明書の検証が完了となりますので、これで「jun-app.com」の証明書の準備ができました。次はcloudfrontとS3を独自ドメインを用いて連携します。",[419,512,514],{"id":513},"cloudfrontとs3を連携","cloudfrontとS3を連携",[13,516,517,518,523],{},"それではcloudfrontとS3を独自ドメインを連携します。",[53,519,522],{"href":520,"rel":521},"https:\u002F\u002Fconsole.aws.amazon.com\u002Fcloudfront\u002Fv3\u002Fhome?#\u002F",[57],"cloudfront","のコンソールに移動して「ディスりビーションの作成」をクリックします。",[13,525,526,527,530],{},"そしてcloudfrontと連携するAWSリソースやオリジンなどを設定します。「オリジンドメイン」でS3リソースを選択するのですがS3をwebサーバーとして利用する場合 ",[82,528,529],{},"サジェストに出てくる 「~~~~.s3.ap-northeast-1.amazonaws.com」 を選択してはいけません。"," これは単純に「S3オブジェクトURL」でありwebサーバーのような動きをしません。",[74,532],{":src":533,":width":77},"'s3-clioudfront-website\u002Fsch-clf-1.png'",[13,535,536,537],{},"今回の構成ではS3の「静的ウェブサイトホスティング」を有効にした際に出現した（下図のボケているとこ）のバケットウェブサイトエンドポイントをいれます。このバケットウェブサイトエンドポイントは「S3をwebサーバーとして使用するときのドメイン」です。ここを忘れて「S3オブジェクトURL」に設定しても一応ルートはs3に接続はできるのですが、「",[53,538,541],{"href":539,"rel":540},"https:\u002F\u002Fjun-app.com\u002Ftest\u002F%E3%80%8D%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B%E3%81%A8%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%8C%E7%99%BA%E7%94%9F%E3%81%97%E3%81%9F%E3%82%8A%E3%80%81%E6%80%9D%E3%81%86%E3%82%88%E3%81%86%E3%81%AB%E5%8B%95%E3%81%8D%E3%81%BE%E3%81%9B%E3%82%93%E3%80%82%E7%B5%90%E6%A7%8B%E3%83%8F%E3%83%9E%E3%82%8A%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%A7%E3%81%99%E3%80%82",[57],"https:\u002F\u002Fjun-app.com\u002Ftest\u002F」のように接続するとエラーが発生したり、思うように動きません。結構ハマりポイントです。",[74,543],{":src":544,":width":77},"'s3-clioudfront-website\u002Fsch-clf-2.png'",[13,546,547],{},"オリジンの設定をしたら他の項目は基本的にデフォルトのままでOKです。下に進んで独自ドメインと証明書を設定する箇所があります。「代替ドメイン名 (CNAME)」に連携したいドメイン名を入力し、そのドメインに対応する証明書（先ほど取得した証明書）を選択します。",[74,549],{":src":550,":width":77},"'s3-clioudfront-website\u002Fsch-clf-3.png'",[13,552,553,554],{},"最後にデフォルトルートオブジェクトに「index.html」を設定します。こうすると「",[53,555,558],{"href":556,"rel":557},"https:\u002F\u002Fjun-app.com\u002Ftest\u002F%E3%80%8D%E3%81%A8%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%97%E3%81%9F%E9%9A%9B%E3%80%8Chttps:\u002F\u002Fjun-app.com\u002Ftest\u002Findex.html%E3%80%8D%E3%81%8C%E8%87%AA%E5%8B%95%E7%9A%84%E3%81%AB%E8%BF%94%E3%81%95%E3%82%8C%E3%81%BE%E3%81%99%E3%80%82",[57],"https:\u002F\u002Fjun-app.com\u002Ftest\u002F」とリクエストした際「https:\u002F\u002Fjun-app.com\u002Ftest\u002Findex.html」が自動的に返されます。",[74,560],{":src":561,":width":77},"'s3-clioudfront-website\u002Fsch-clf-4.png'",[13,563,564,565,568,569,571],{},"これで一通りcloudfrontとS3の連携が完了しました。「ディストリビーションを作成」を行いますと一覧にて ",[102,566,567],{},"~~~~~~.cloudfront.net"," というS3オリジンに対するcloudfrontのドメインが作成されます。このドメインにアクセスしてみますと、SSL付きでS3上にアップロードした ",[102,570,104],{}," が表示されます。",[13,573,574,575,578],{},"つぎは cloudfrontのドメインが ",[102,576,577],{},"jun-app.com"," に向くようにRoute53を設定します。",[419,580,581],{"id":581},"cloudfrontのドメインを独自ドメインに向ける",[13,583,584],{},"ではRoute53に戻ってcloudfrontのドメインが独自ドメインに向くようにします。ドメインのホストゾーンにて「レコードの作成」を選択します。そして以下のような画面が開きます。「レコード名」はサブドメインを作るわけでないので、空欄で大丈夫です。そしてレコードタイプは「A - ipvアドレスと..」を選択します。「トラフィックのルーティング先」のエイリアスをONにします。（トグルになっています）",[13,586,587],{},"Aレコードは普通、IPアドレスを入力しますがcloudfrontなどのAWSリソースでしているする場合「エイリアス」を使用します。エイリアスでは「CloudFront ディストリビューションへのエイリアス」を選択します。そしてその下にcloudfrontのディストリビューションの入力欄が現れますので、そこに先ほどのcloudfrontのドメインを入力します。",[74,589],{":src":590,":width":77},"'s3-clioudfront-website\u002Fsch-clf-r53-1.png'",[39,592,595],{"className":593},[42,594],"alert-info","\n本来であれば登録したcloudfrontのディストリビューションがサジェストに表示されますが、登録したてだと出てこないことがあります。ただディストリビューションがあればとりあえず登録できます。\n",[13,597,598,599,601],{},"そして最後に「レコードの作成」をクリックします。すると ",[102,600,577],{}," に対するAレコードがcloudfrontのディストリビューションに向くようになります。ここまでドメインの流れを追うと以下の通りになります。",[398,603,604,609,622],{},[20,605,606,608],{},[102,607,577],{}," にリクエストしたとき、まずお名前.comに問い合わせるが「Route53」に委任されているのでRoute53へ問い合わせ",[20,610,611,612,614,615,618,619,621],{},"Route53では ",[102,613,577],{}," のAレコードが ",[102,616,617],{},"xxxx.cloudfront.net"," に向いているので、",[102,620,617],{},"へ接続。",[20,623,624,626],{},[102,625,617],{}," は S3のwebホスティングURLに向いているので、そこに接続",[13,628,629],{},"となります。（結構ざっくりとしてます）\n以上でバックエンド側の準備ができました。すでにドメインを使用している場合はDNSキャッシュが効いていることがあります。別の端末からアクセスしたり、DNSキャッシュをクリアするコマンドを打ってみましょう。",[39,631,634,635,638,639,642],{"className":632},[42,633],"alert-success","\nDNSの設定がうまく、対象のサーバーに向いているかを調べるときは",[102,636,637],{},"nslookup","や",[102,640,641],{},"dig","コマンドを使って対象ドメインに紐づけられているサーバーを確かめてみましょう。\n",[13,644,645,646,648,649,651],{},"現在のところS3に",[102,647,104],{},"が置いてあれば、独自ドメイン＋SSLでその",[102,650,104],{},"が表示されていればOKです。",[46,653,655],{"id":654},"aws-cliを用いたs3へのファイルのデプロイ","AWS CLIを用いたS3へのファイルのデプロイ",[13,657,658,659,662],{},"では最後にブログファイル群をS3にアップロードします。私の場合はNuxt.jsであらかじめ書き出しを行い、",[102,660,661],{},"dist","ファイル配下を送信します。手動は毎回大変ですのでAWS CLIを用いて自動化と差分アップロード できるようにします。その手順を解説します。最初にAWS CLIをもちいて対象バケットにアクセスできるIAMユーザーを作成します。",[419,664,666],{"id":665},"iamでデプロイユーザーを作成する","IAMでデプロイユーザーを作成する",[13,668,669],{},"AWSの運用ベストプラクティスでは",[671,672,673],"blockquote",{},[13,674,675],{},"AWS アカウントのルートユーザー のアクセスキーを使用しないでください。\nIAM により、複数のユーザーに AWS リソースへの安全なアクセスを簡単に提供できます。IAM により以下が可能となります。",[13,677,678],{},"とある様に基本的にIAMというものを用いてリソースにアクセスする様にします。例えば今回の様にAWS CLIを用いて自分のAWSリソースにアクセスする時にシークレットキーとアクセスキーを使用します。AWSを始めた時に作ったRootアカウントでも可能ですが、rootはCLIを通じで本当になんでもできてしまうので使うべきではありません。であればアクションとアクセスを制限されたユーザー（IAM）を使用すれば、もしキーが漏れたとしても被害は小さめです。なので今回のブログデプロイ用のIAMを作成してしまいます。",[13,680,681],{},"AWSにログインしてIAMに移動し、「ユーザーを追加」をクリックします。",[74,683],{":src":684,":width":77},"'s3-clioudfront-website\u002Fsch-iam-0.png'",[13,686,687],{},"ユーザー名を設定し、「プログラムによるアクセス」にチェックをし、次のアクセス権限の設定を行います。",[74,689],{":src":690,":width":77},"'s3-clioudfront-website\u002Fsch-iam-1.png'",[13,692,693],{},"ここでこのユーザーがアクセスできるアクションを設定できます。一応JSONを用いて細かい設定もできますが、今回はチュートリアル的な内容なので「AmazonS3FullAccess」を与えておきます。",[74,695],{":src":696,":width":77},"'s3-clioudfront-website\u002Fsch-iam-2.png'",[13,698,699],{},"次にタグを設定しますが、組織でなければ特に設定しません。最後に全体を確認して「ユーザーを作成」をします。これでS3だけにアクセスできるIAMができます。",[74,701],{":src":702,":width":77},"'s3-clioudfront-website\u002Fsch-iam-3.png'",[13,704,705],{},"このユーザー用のアクセスキーとシークレットキーがダウンロードできますので、控えておきます。まずデプロイ用のIAMユーザーができました。AWS CLIのインストールはここでは割愛しまが、ここで取得したIAMのキーをプロファイルに設定します。",[137,707,711],{"className":708,"code":709,"language":710,"meta":142,"style":142},"language-bash shiki shiki-themes material-theme-ocean","aws configure --profile junapp-s3\n","bash",[102,712,713],{"__ignoreMap":142},[146,714,715,718,721,724],{"class":148,"line":149},[146,716,717],{"class":211},"aws",[146,719,720],{"class":175}," configure",[146,722,723],{"class":175}," --profile",[146,725,726],{"class":175}," junapp-s3\n",[13,728,729],{},"プロファイル名は分かりやすい名前にしておくといいです。これでIAMと転送のセットアップは完了です。",[419,731,733],{"id":732},"ファイルを生成してs3へアップロード","ファイルを生成してS3へアップロード",[13,735,736,737,740,741,743,744,746,747,750],{},"私の環境では ",[102,738,739],{},"npm run generate"," で ",[102,742,661],{}," ファイルが生成され、必要なHTMLファイルとアセットが出力されます。その ",[102,745,661],{},"ファイル配下を全て",[102,748,749],{},"s3:\u002F\u002Fjun-app.com\u002F","へ転送します。そのときはAWS CLIで以下のコマンドを使用します。",[137,752,754],{"className":708,"code":753,"language":710,"meta":142,"style":142},"aws s3 sync .\u002Fdist\u002F s3:\u002F\u002Fjun-app.com\u002F --delete --profile junapp-s3\n",[102,755,756],{"__ignoreMap":142},[146,757,758,760,763,766,769,772,775,777],{"class":148,"line":149},[146,759,717],{"class":211},[146,761,762],{"class":175}," s3",[146,764,765],{"class":175}," sync",[146,767,768],{"class":175}," .\u002Fdist\u002F",[146,770,771],{"class":175}," s3:\u002F\u002Fjun-app.com\u002F",[146,773,774],{"class":175}," --delete",[146,776,723],{"class":175},[146,778,726],{"class":175},[13,780,781,784,785,788,789,792,793,796,797,800],{},[102,782,783],{},"s3 sync","は2回目以降、差分をみてあれば同期してくれます。二回目以降は転送量を節約できます。",[102,786,787],{},"--delete"," は同期先（S3）にて同期元（ローカル）にないファイルを消してくれます。例えばnuxt.jsではキャッシュ対策のため、",[102,790,791],{},"_nuxt","ディレクトリ配下に",[102,794,795],{},"34234324.js","みたいなハッシュ化したjsファイルが書き出しごとに生成されます。単純に転送していくと",[102,798,799],{},"__nuxt","配下にjsファイルが溜まっていくので差分を見て古いファイルを削除します。他にも「メニューなどにはないけれど、削除した記事がファイルとしては残ったままになっている」みたいな事故も防げます。",[13,802,803,806],{},[102,804,805],{},"--profile"," にて先ほど設定したIAMのプロファイルを指定します。",[39,808,810,811,814],{"className":809},[42,594],"\nこのコマンドを実行する前に ",[102,812,813],{},"--dryrun","のオプションをいれてシミュレートしてから本番を実行しましょう。\n",[13,816,817],{},"準備ができたらコマンドを打ちます。終わったらバケットを確認して終了です。",[819,820,821],"h4",{"id":821},"スクリプトに入れておくと便利",[13,823,824,825,827,828,831,832,835],{},"Nuxt.jsで",[102,826,739],{},"した際に",[102,829,830],{},"sync","されるように",[102,833,834],{},"package.json","を以下のように編集しておきました。",[137,837,842],{"className":838,"code":839,"filename":840,"language":841,"meta":142,"style":142},"language-json shiki shiki-themes material-theme-ocean","{\n\"scripts\": {\n    ...\n    \"generate-production\": \"nuxt generate && aws s3 sync .\u002Fdist\u002F s3:\u002F\u002Fjun-app.com\u002F --delete --profile junapp-s3\"\n}\n}\n","pakcage.json","json",[102,843,844,848,862,868,886,890],{"__ignoreMap":142},[146,845,846],{"class":148,"line":149},[146,847,153],{"class":152},[146,849,850,852,855,857,859],{"class":148,"line":156},[146,851,166],{"class":152},[146,853,854],{"class":162},"scripts",[146,856,166],{"class":152},[146,858,169],{"class":152},[146,860,861],{"class":152}," {\n",[146,863,864],{"class":148,"line":184},[146,865,867],{"class":866},"s0W1g","    ...\n",[146,869,870,872,875,877,879,881,884],{"class":148,"line":199},[146,871,159],{"class":152},[146,873,874],{"class":211},"generate-production",[146,876,166],{"class":152},[146,878,169],{"class":152},[146,880,172],{"class":152},[146,882,883],{"class":175},"nuxt generate && aws s3 sync .\u002Fdist\u002F s3:\u002F\u002Fjun-app.com\u002F --delete --profile junapp-s3",[146,885,293],{"class":152},[146,887,888],{"class":148,"line":205},[146,889,347],{"class":152},[146,891,892],{"class":148,"line":228},[146,893,347],{"class":152},[819,895,896],{"id":896},"cloudfrontのキャッシュに注意",[13,898,899],{},"cloudfrontはCDNであり、キャッシュが強いです。更新したのに本番が変わらないときはキャッシュのせいかもしれません。cloudfrontでは明示的にキャッシュをクリアすることもできますし、キャッシュの期間を変更することもできます。閲覧数などに合わせて設定しましょう。",[46,901,903],{"id":902},"以上","以上！",[13,905,906],{},"以上で S3 x SSL x 独自ドメインな静的ブログを構築できました。利用量によっては無料枠でも収まりそうです。しばらく料金の方も見てみてレンサバよりお得かを確認してみます。",[908,909,910],"style",{},"html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}",{"title":142,"searchDepth":184,"depth":184,"links":912},[913,914,915,921,928],{"id":48,"depth":156,"text":48},{"id":68,"depth":156,"text":69},{"id":392,"depth":156,"text":393,"children":916},[917,918,919,920],{"id":421,"depth":184,"text":422},{"id":467,"depth":184,"text":467},{"id":513,"depth":184,"text":514},{"id":581,"depth":184,"text":581},{"id":654,"depth":156,"text":655,"children":922},[923,924],{"id":665,"depth":184,"text":666},{"id":732,"depth":184,"text":733,"children":925},[926,927],{"id":821,"depth":199,"text":821},{"id":896,"depth":199,"text":896},{"id":902,"depth":156,"text":903},[930],"devstack","2025-07-18","AWS S3とcloudfrontを合わせることでwebサーバの様に扱う方法","md",null,{},true,"\u002Farticles\u002Fs3-clioudfront-website",{"title":8,"description":932},"articles\u002Fs3-clioudfront-website",[941,717],"infrastructure","s3-clioudfront-website\u002Fthumbnail.png","TS4Cse0E-KmfWg0S6g6WJdYqcKJXhparmDnjSsmNt20",{"id":945,"title":946,"body":947,"category":2396,"createdAt":2397,"description":2398,"extension":933,"index":934,"meta":2399,"navigation":936,"path":2400,"publish":936,"seo":2401,"series":934,"seriesTitle":934,"stem":2402,"tag":2403,"thumbnail":2405,"updatedAt":934,"__hash__":2406},"articles\u002Farticles\u002F2024_09_22_line_great_circle_antimeridian_cutting.md","turf.jsを用いたメルカトル図法地図上の線の大圏航路補正・逆子午線分割",{"type":10,"value":948,"toc":2389},[949,952,955,959,962,966,969,972,975,978,981,984,987,990,993,996,999,1002,1006,1009,1013,1016,1756,1759,2386],[13,950,951],{},"私がメルカトル図法を使用した地図上で、mapboxに2点以上の線を引く処理を実装していたとき、線が直線的に描画されるという問題が発生しました。さらに、経度180度の逆子午線を跨ぐ場合、mapboxでは大回りの線が描かれてしまい、その修正も必要でした。例えば、東京→ロサンゼルスの２点を結ぶと、東経180度を越えるのでなく、西回りでぐるっと線が引かれてしまいました。",[13,953,954],{},"本記事では、JavaScriptライブラリであるturf.jsを用いて、補正と分割を行った表示用のパスを算出手法について紹介します。",[46,956,958],{"id":957},"大圏航路とはなぜ補正が必要","大圏航路とは？なぜ補正が必要？",[13,960,961],{},"大圏航路（Great Circle Route）は、地球上の2点を最短距離で結ぶ経路です。なんとなく２点を結んだ直線が最短経路と思ってしましますが、実際はそうではありません。よく見る平面の地図はメルカトル図法という方法で表現されており、引いた線が実際の地球上の経路、距離、面積と一致しません。これは地球が球面であり、球面上の線を平面上の線の描画と一致しないからです。例えば、東京→デリーの最短ルートは実際以下の通りで、直線的に結んだものではありません。（左が補正なしの直線）",[74,963],{":src":964,":width":965},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig2.png'","'200px'",[74,967],{":src":968,":width":965},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig1.png'",[13,970,971],{},"メルカトル図法は、緯度が高くなるほど距離の比率が歪むため、大圏航路を描くと地図上で曲線として表示されるのが特徴です。しかし、通常のGeoJSONなどを使用して単純に線を引くと、2点間を直線で結んでしまい、大圏航路としての正確さが失れます。",[13,973,974],{},"これは2点の距離が離れるほど、平面上に引いた直線と実際の線と乖離します。短い距離、少なくとも日本列島ぐらいであれば問題ありませんが、大陸レベルだったり海路・空路を表現するときはその乖離が顕著になります。",[13,976,977],{},"海路・空路を記載するような地図はこの補正を考慮しないといけません。わかりやすくすると以下の通りです。",[74,979],{":src":980,":width":965},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig5.png'",[74,982],{":src":983,":width":965},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig6.png'",[46,985,986],{"id":986},"逆子午線を越えるときには分割が必要",[13,988,989],{},"逆子午線とは、経度180度の線を指します。mapboxで経度180度を跨ぐ線を描こうとすると、通常の経路補正では大回りで描かれてしまい、直感的に不自然になります。",[74,991],{":src":992,":width":965},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig3.png'",[74,994],{":src":995,":width":965},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig4.png'",[13,997,998],{},"そのため、逆子午線を越える線を正しく描画するには、線を分割して複数のLineStringやMultiLineStringに分ける処理を行いました。図としてこのような感じです。",[74,1000],{":src":1001,":width":965},"'2024_09_22_line_great_circle_antimeridian_cutting\u002Ffig7.png'",[46,1003,1005],{"id":1004},"基本的にturfjsを使えば解決できる","基本的にturf.jsを使えば解決できる",[13,1007,1008],{},"JavaScriptのturf.jsライブラリを用いることで、これらの問題を解決できます。turf.jsはGeoJSONデータの操作に強力な機能を提供しており、大圏航路の計算を行うとき、180度を越えるとき自動的に分割したパスを渡してくれます。",[419,1010,1012],{"id":1011},"_2点間に補正点を算出","2点間に補正点を算出",[13,1014,1015],{},"まず、2点間の線を大圏航路で補正するために、turf.jsのturf.greatCircle関数を使用しました。この関数を使うことで2点間を大圏航路で補正し、曲線的な追加のパスを生成できます。\n180度を超えない2点の場合、補正した緯度経度の配列が戻ります。180度を越える場合、180度で分割した2つのlineの緯度経度配列が戻ります。",[137,1017,1021],{"className":1018,"code":1019,"language":1020,"meta":142,"style":142},"language-ts shiki shiki-themes material-theme-ocean","import * as turf from '@turf\u002Fturf';\nimport { Feature } from \"geojson\";\n\nconst path = [lat:number,lng:number][]\nconst displayPath: [number, number][][] = [];\n\n\u002F\u002F 各ラインのパスごとに大円航路を計算したパスを追加\nfor (let i = 0; i \u003C path.length - 1; i++) {\n    const startPoint = path[i];\n    const endPoint = path[i + 1];\n\n    const currentPoint = turf.point([startPoint[1], startPoint[0]]); \u002F\u002F turf.js は経度・緯度と入力したり戻ってくるので注意。\n    const nextPoint = turf.point([endPoint[1], endPoint[0]]);\n\n    const greatCircle = turf.greatCircle(currentPoint, nextPoint, { npoints: 30 }); \u002F\u002F npoints が追加する補正点。多いほど滑らか\n\n    const coordinates = greatCircle.geometry.coordinates;\n\n    \u002F\u002F LineString の場合\n    if (Array.isArray(coordinates[0]) && !Array.isArray(coordinates[0][0])) {\n        displayPath.push(coordinates as [number, number][]);\n    } \n    \u002F\u002F MultiLineString の場合\n    else if (Array.isArray(coordinates[0][0])) {\n        coordinates.forEach((segment) => {\n            displayPath.push(segment as [number, number][]);\n        });\n    }\n}\n\nconst geojson = {\n    type: \"Feature\",\n    properties: {},\n    geometry: {\n        type: \"MultiLineString\",\n        coordinates: displayPath,\n    },\n} as Feature;\n\nreturn geojson;\n","ts",[102,1022,1023,1053,1078,1083,1103,1133,1137,1143,1199,1224,1248,1252,1300,1338,1342,1390,1394,1418,1423,1429,1486,1516,1525,1531,1564,1590,1618,1628,1634,1639,1644,1656,1673,1684,1694,1711,1722,1728,1740,1745],{"__ignoreMap":142},[146,1024,1025,1029,1032,1035,1038,1041,1044,1047,1050],{"class":148,"line":149},[146,1026,1028],{"class":1027},"s6cf3","import",[146,1030,1031],{"class":152}," *",[146,1033,1034],{"class":1027}," as",[146,1036,1037],{"class":866}," turf ",[146,1039,1040],{"class":1027},"from",[146,1042,1043],{"class":152}," '",[146,1045,1046],{"class":175},"@turf\u002Fturf",[146,1048,1049],{"class":152},"'",[146,1051,1052],{"class":152},";\n",[146,1054,1055,1057,1060,1063,1066,1069,1071,1074,1076],{"class":148,"line":156},[146,1056,1028],{"class":1027},[146,1058,1059],{"class":152}," {",[146,1061,1062],{"class":866}," Feature",[146,1064,1065],{"class":152}," }",[146,1067,1068],{"class":1027}," from",[146,1070,172],{"class":152},[146,1072,1073],{"class":175},"geojson",[146,1075,166],{"class":152},[146,1077,1052],{"class":152},[146,1079,1080],{"class":148,"line":184},[146,1081,1082],{"emptyLinePlaceholder":936},"\n",[146,1084,1085,1088,1091,1094,1097,1100],{"class":148,"line":199},[146,1086,1087],{"class":162},"const",[146,1089,1090],{"class":866}," path ",[146,1092,1093],{"class":152},"=",[146,1095,1096],{"class":866}," [lat:number",[146,1098,1099],{"class":152},",",[146,1101,1102],{"class":866},"lng:number][]\n",[146,1104,1105,1107,1110,1112,1115,1118,1120,1123,1126,1128,1131],{"class":148,"line":205},[146,1106,1087],{"class":162},[146,1108,1109],{"class":866}," displayPath",[146,1111,169],{"class":152},[146,1113,1114],{"class":866}," [",[146,1116,1117],{"class":211},"number",[146,1119,1099],{"class":152},[146,1121,1122],{"class":211}," number",[146,1124,1125],{"class":866},"][][] ",[146,1127,1093],{"class":152},[146,1129,1130],{"class":866}," []",[146,1132,1052],{"class":152},[146,1134,1135],{"class":148,"line":228},[146,1136,1082],{"emptyLinePlaceholder":936},[146,1138,1139],{"class":148,"line":249},[146,1140,1142],{"class":1141},"sC9rS","\u002F\u002F 各ラインのパスごとに大円航路を計算したパスを追加\n",[146,1144,1145,1148,1151,1154,1157,1159,1163,1166,1168,1171,1174,1177,1180,1183,1186,1188,1191,1194,1197],{"class":148,"line":270},[146,1146,1147],{"class":1027},"for",[146,1149,1150],{"class":866}," (",[146,1152,1153],{"class":162},"let",[146,1155,1156],{"class":866}," i ",[146,1158,1093],{"class":152},[146,1160,1162],{"class":1161},"sx098"," 0",[146,1164,1165],{"class":152},";",[146,1167,1156],{"class":866},[146,1169,1170],{"class":152},"\u003C",[146,1172,1173],{"class":866}," path",[146,1175,1176],{"class":152},".",[146,1178,1179],{"class":866},"length ",[146,1181,1182],{"class":152},"-",[146,1184,1185],{"class":1161}," 1",[146,1187,1165],{"class":152},[146,1189,1190],{"class":866}," i",[146,1192,1193],{"class":152},"++",[146,1195,1196],{"class":866},") ",[146,1198,153],{"class":152},[146,1200,1201,1204,1207,1210,1212,1216,1219,1222],{"class":148,"line":284},[146,1202,1203],{"class":162},"    const",[146,1205,1206],{"class":866}," startPoint",[146,1208,1209],{"class":152}," =",[146,1211,1173],{"class":866},[146,1213,1215],{"class":1214},"s-wAU","[",[146,1217,1218],{"class":866},"i",[146,1220,1221],{"class":1214},"]",[146,1223,1052],{"class":152},[146,1225,1226,1228,1231,1233,1235,1237,1239,1242,1244,1246],{"class":148,"line":296},[146,1227,1203],{"class":162},[146,1229,1230],{"class":866}," endPoint",[146,1232,1209],{"class":152},[146,1234,1173],{"class":866},[146,1236,1215],{"class":1214},[146,1238,1218],{"class":866},[146,1240,1241],{"class":152}," +",[146,1243,1185],{"class":1161},[146,1245,1221],{"class":1214},[146,1247,1052],{"class":152},[146,1249,1250],{"class":148,"line":302},[146,1251,1082],{"emptyLinePlaceholder":936},[146,1253,1254,1256,1259,1261,1264,1266,1270,1273,1276,1278,1281,1283,1285,1287,1289,1292,1295,1297],{"class":148,"line":316},[146,1255,1203],{"class":162},[146,1257,1258],{"class":866}," currentPoint",[146,1260,1209],{"class":152},[146,1262,1263],{"class":866}," turf",[146,1265,1176],{"class":152},[146,1267,1269],{"class":1268},"sdLwU","point",[146,1271,1272],{"class":1214},"([",[146,1274,1275],{"class":866},"startPoint",[146,1277,1215],{"class":1214},[146,1279,1280],{"class":1161},"1",[146,1282,1221],{"class":1214},[146,1284,1099],{"class":152},[146,1286,1206],{"class":866},[146,1288,1215],{"class":1214},[146,1290,1291],{"class":1161},"0",[146,1293,1294],{"class":1214},"]])",[146,1296,1165],{"class":152},[146,1298,1299],{"class":1141}," \u002F\u002F turf.js は経度・緯度と入力したり戻ってくるので注意。\n",[146,1301,1302,1304,1307,1309,1311,1313,1315,1317,1320,1322,1324,1326,1328,1330,1332,1334,1336],{"class":148,"line":326},[146,1303,1203],{"class":162},[146,1305,1306],{"class":866}," nextPoint",[146,1308,1209],{"class":152},[146,1310,1263],{"class":866},[146,1312,1176],{"class":152},[146,1314,1269],{"class":1268},[146,1316,1272],{"class":1214},[146,1318,1319],{"class":866},"endPoint",[146,1321,1215],{"class":1214},[146,1323,1280],{"class":1161},[146,1325,1221],{"class":1214},[146,1327,1099],{"class":152},[146,1329,1230],{"class":866},[146,1331,1215],{"class":1214},[146,1333,1291],{"class":1161},[146,1335,1294],{"class":1214},[146,1337,1052],{"class":152},[146,1339,1340],{"class":148,"line":332},[146,1341,1082],{"emptyLinePlaceholder":936},[146,1343,1344,1346,1349,1351,1353,1355,1358,1361,1364,1366,1368,1370,1372,1375,1377,1380,1382,1385,1387],{"class":148,"line":338},[146,1345,1203],{"class":162},[146,1347,1348],{"class":866}," greatCircle",[146,1350,1209],{"class":152},[146,1352,1263],{"class":866},[146,1354,1176],{"class":152},[146,1356,1357],{"class":1268},"greatCircle",[146,1359,1360],{"class":1214},"(",[146,1362,1363],{"class":866},"currentPoint",[146,1365,1099],{"class":152},[146,1367,1306],{"class":866},[146,1369,1099],{"class":152},[146,1371,1059],{"class":152},[146,1373,1374],{"class":1214}," npoints",[146,1376,169],{"class":152},[146,1378,1379],{"class":1161}," 30",[146,1381,1065],{"class":152},[146,1383,1384],{"class":1214},")",[146,1386,1165],{"class":152},[146,1388,1389],{"class":1141}," \u002F\u002F npoints が追加する補正点。多いほど滑らか\n",[146,1391,1392],{"class":148,"line":344},[146,1393,1082],{"emptyLinePlaceholder":936},[146,1395,1397,1399,1402,1404,1406,1408,1411,1413,1416],{"class":148,"line":1396},17,[146,1398,1203],{"class":162},[146,1400,1401],{"class":866}," coordinates",[146,1403,1209],{"class":152},[146,1405,1348],{"class":866},[146,1407,1176],{"class":152},[146,1409,1410],{"class":866},"geometry",[146,1412,1176],{"class":152},[146,1414,1415],{"class":866},"coordinates",[146,1417,1052],{"class":152},[146,1419,1421],{"class":148,"line":1420},18,[146,1422,1082],{"emptyLinePlaceholder":936},[146,1424,1426],{"class":148,"line":1425},19,[146,1427,1428],{"class":1141},"    \u002F\u002F LineString の場合\n",[146,1430,1432,1435,1437,1440,1442,1445,1447,1449,1451,1453,1456,1459,1462,1464,1466,1468,1470,1472,1474,1476,1479,1481,1484],{"class":148,"line":1431},20,[146,1433,1434],{"class":1027},"    if",[146,1436,1150],{"class":1214},[146,1438,1439],{"class":866},"Array",[146,1441,1176],{"class":152},[146,1443,1444],{"class":1268},"isArray",[146,1446,1360],{"class":1214},[146,1448,1415],{"class":866},[146,1450,1215],{"class":1214},[146,1452,1291],{"class":1161},[146,1454,1455],{"class":1214},"]) ",[146,1457,1458],{"class":152},"&&",[146,1460,1461],{"class":152}," !",[146,1463,1439],{"class":866},[146,1465,1176],{"class":152},[146,1467,1444],{"class":1268},[146,1469,1360],{"class":1214},[146,1471,1415],{"class":866},[146,1473,1215],{"class":1214},[146,1475,1291],{"class":1161},[146,1477,1478],{"class":1214},"][",[146,1480,1291],{"class":1161},[146,1482,1483],{"class":1214},"])) ",[146,1485,153],{"class":152},[146,1487,1489,1492,1494,1497,1499,1501,1503,1505,1507,1509,1511,1514],{"class":148,"line":1488},21,[146,1490,1491],{"class":866},"        displayPath",[146,1493,1176],{"class":152},[146,1495,1496],{"class":1268},"push",[146,1498,1360],{"class":1214},[146,1500,1415],{"class":866},[146,1502,1034],{"class":1027},[146,1504,1114],{"class":1214},[146,1506,1117],{"class":211},[146,1508,1099],{"class":152},[146,1510,1122],{"class":211},[146,1512,1513],{"class":1214},"][])",[146,1515,1052],{"class":152},[146,1517,1519,1522],{"class":148,"line":1518},22,[146,1520,1521],{"class":152},"    }",[146,1523,1524],{"class":1214}," \n",[146,1526,1528],{"class":148,"line":1527},23,[146,1529,1530],{"class":1141},"    \u002F\u002F MultiLineString の場合\n",[146,1532,1534,1537,1540,1542,1544,1546,1548,1550,1552,1554,1556,1558,1560,1562],{"class":148,"line":1533},24,[146,1535,1536],{"class":1027},"    else",[146,1538,1539],{"class":1027}," if",[146,1541,1150],{"class":1214},[146,1543,1439],{"class":866},[146,1545,1176],{"class":152},[146,1547,1444],{"class":1268},[146,1549,1360],{"class":1214},[146,1551,1415],{"class":866},[146,1553,1215],{"class":1214},[146,1555,1291],{"class":1161},[146,1557,1478],{"class":1214},[146,1559,1291],{"class":1161},[146,1561,1483],{"class":1214},[146,1563,153],{"class":152},[146,1565,1567,1570,1572,1575,1577,1579,1583,1585,1588],{"class":148,"line":1566},25,[146,1568,1569],{"class":866},"        coordinates",[146,1571,1176],{"class":152},[146,1573,1574],{"class":1268},"forEach",[146,1576,1360],{"class":1214},[146,1578,1360],{"class":152},[146,1580,1582],{"class":1581},"s7ZW3","segment",[146,1584,1384],{"class":152},[146,1586,1587],{"class":162}," =>",[146,1589,861],{"class":152},[146,1591,1593,1596,1598,1600,1602,1604,1606,1608,1610,1612,1614,1616],{"class":148,"line":1592},26,[146,1594,1595],{"class":866},"            displayPath",[146,1597,1176],{"class":152},[146,1599,1496],{"class":1268},[146,1601,1360],{"class":1214},[146,1603,1582],{"class":866},[146,1605,1034],{"class":1027},[146,1607,1114],{"class":1214},[146,1609,1117],{"class":211},[146,1611,1099],{"class":152},[146,1613,1122],{"class":211},[146,1615,1513],{"class":1214},[146,1617,1052],{"class":152},[146,1619,1621,1624,1626],{"class":148,"line":1620},27,[146,1622,1623],{"class":152},"        }",[146,1625,1384],{"class":1214},[146,1627,1052],{"class":152},[146,1629,1631],{"class":148,"line":1630},28,[146,1632,1633],{"class":152},"    }\n",[146,1635,1637],{"class":148,"line":1636},29,[146,1638,347],{"class":152},[146,1640,1642],{"class":148,"line":1641},30,[146,1643,1082],{"emptyLinePlaceholder":936},[146,1645,1647,1649,1652,1654],{"class":148,"line":1646},31,[146,1648,1087],{"class":162},[146,1650,1651],{"class":866}," geojson ",[146,1653,1093],{"class":152},[146,1655,861],{"class":152},[146,1657,1659,1662,1664,1666,1669,1671],{"class":148,"line":1658},32,[146,1660,1661],{"class":1214},"    type",[146,1663,169],{"class":152},[146,1665,172],{"class":152},[146,1667,1668],{"class":175},"Feature",[146,1670,166],{"class":152},[146,1672,181],{"class":152},[146,1674,1676,1679,1681],{"class":148,"line":1675},33,[146,1677,1678],{"class":1214},"    properties",[146,1680,169],{"class":152},[146,1682,1683],{"class":152}," {},\n",[146,1685,1687,1690,1692],{"class":148,"line":1686},34,[146,1688,1689],{"class":1214},"    geometry",[146,1691,169],{"class":152},[146,1693,861],{"class":152},[146,1695,1697,1700,1702,1704,1707,1709],{"class":148,"line":1696},35,[146,1698,1699],{"class":1214},"        type",[146,1701,169],{"class":152},[146,1703,172],{"class":152},[146,1705,1706],{"class":175},"MultiLineString",[146,1708,166],{"class":152},[146,1710,181],{"class":152},[146,1712,1714,1716,1718,1720],{"class":148,"line":1713},36,[146,1715,1569],{"class":1214},[146,1717,169],{"class":152},[146,1719,1109],{"class":866},[146,1721,181],{"class":152},[146,1723,1725],{"class":148,"line":1724},37,[146,1726,1727],{"class":152},"    },\n",[146,1729,1731,1734,1736,1738],{"class":148,"line":1730},38,[146,1732,1733],{"class":152},"}",[146,1735,1034],{"class":1027},[146,1737,1062],{"class":211},[146,1739,1052],{"class":152},[146,1741,1743],{"class":148,"line":1742},39,[146,1744,1082],{"emptyLinePlaceholder":936},[146,1746,1748,1751,1754],{"class":148,"line":1747},40,[146,1749,1750],{"class":1027},"return",[146,1752,1753],{"class":866}," geojson",[146,1755,1052],{"class":152},[13,1757,1758],{},"わかりやすいように、180度を越える・超えないパターンでの大圏航路の補正です。",[137,1760,1764],{"className":1761,"code":1762,"language":1763,"meta":142,"style":142},"language-js shiki shiki-themes material-theme-ocean","import * as turf from '@turf\u002Fturf';\n\nconst nonOver = turf.greatCircle(\n    [139.6503, 35.6762],  \u002F\u002F 東京の座標\n    [77.2090, 28.6139], \u002F\u002F デリーの座標\n    { npoints: 30 }\n);\n\nconsole.log(nonOver)\n\u002F**\n[\n  [ 139.6503, 35.67620000000001 ],\n  [ 137.45530635075792, 36.00345099838847 ],\n  [ 135.24315241142824, 36.29039534292487 ],\n  [ 133.01584313143206, 36.53630839532294 ],\n  [ 130.77553308674652, 36.74055732953035 ],\n  [ 128.52450866279412, 36.90260805727012 ],\n  [ 126.26516729530344, 37.022031252284606 ],\n  [ 123.99999414238111, 37.09850731283235 ],\n  [ 121.73153667001746, 37.131830125727525 ],\n  [ 119.46237772511748, 37.121909525547366 ],\n  [ 117.19510773852923, 37.0687723782786 ],\n  [ 114.93229674053903, 36.972562257930214 ],\n  [ 112.67646687998554, 36.83353772552399 ],\n  [ 110.43006611487587, 36.65206926027127 ],\n  [ 108.19544368887564, 36.42863493057574 ],\n  [ 105.97482792817907, 36.163814925900674 ],\n  [ 103.7703067926894, 35.858285097980165 ],\n  [ 101.58381150098748, 35.51280968026889 ],\n  [ 99.41710342759748, 35.12823336735064 ],\n  [ 97.27176435078754, 34.705472941209955 ],\n  [ 95.14919001606306, 34.245508629231686 ],\n  [ 93.05058687993017, 33.749375370334334 ],\n  [ 90.97697181428099, 33.21815415185492 ],\n  [ 88.92917448617246, 32.6529635619425 ],\n  [ 86.90784208162192, 32.05495168160128 ],\n  [ 84.91344601479491, 31.42528841842546 ],\n  [ 82.94629025402638, 30.7651583616406 ],\n  [ 81.00652090116452, 30.075754216293156 ],\n  [ 79.09413667798896, 29.358270854084918 ],\n  [ 77.209, 28.613900000000005 ]\n]\n*\u002F\n\nconst over = turf.greatCircle(\n    [139.6503, 35.6762],  \u002F\u002F 東京の座標\n    [-147.7164, 64.8378], \u002F\u002F アラスカの座標\n    { npoints: 30 }\n);\n\nconsole.log(over)\n\u002F*\n  [\n    [ 139.6503, 35.67620000000001 ],\n    [ 140.80178756980726, 37.16607636013253 ],\n    [ 141.99941083018345, 38.64436492965305 ],\n    [ 143.24727313996488, 40.10993078543407 ],\n    [ 144.54983478842868, 41.56151748104379 ],\n    [ 145.91194328157903, 42.9977313513525 ],\n    [ 147.33886324769875, 44.41702385324905 ],\n    [ 148.83630454782175, 45.81767177569259 ],\n    [ 150.41044655311683, 47.19775518537249 ],\n    [ 152.0679557258351, 48.55513303641521 ],\n    [ 153.8159925691613, 49.88741647693477 ],\n    [ 155.66220265313686, 51.19194004906208 ],\n    [ 157.6146847535422, 52.465731224424154 ],\n    [ 159.68192717003024, 53.705479070518045 ],\n    [ 161.87270110244128, 54.90750333482882 ],\n    [ 164.19589776721827, 56.06772589186521 ],\n    [ 166.66029413034695, 57.18164734362052 ],\n    [ 169.27423139783608, 58.24433259326387 ],\n    [ 172.0451917704491, 59.25041037671838 ],\n    [ 174.9792638386216, 60.19409291290832 ],\n    [ 178.08049701800238, 61.069222785803966 ],\n    [ 180, 61.53895140096846 ]\n  ],\n  [\n    [ -180, 61.53895140096846 ],\n    [ -178.64983787529644, 61.86935452669037 ],\n    [ -175.2140408098754, 62.587877615485425 ],\n    [ -171.618756489847, 63.21818519285117 ],\n    [ -167.87562800253605, 63.753888204781546 ],\n    [ -164.0017045661598, 64.18906791167207 ],\n    [ -160.0194735821535, 64.51855132598804 ],\n    [ -155.9564136218279, 64.73818576863974 ],\n    [ -151.84402691822368, 64.84508272020929 ],\n    [ -147.7164, 64.8378 ]\n  ]\n]\n*\u002F\n","js",[102,1765,1766,1786,1790,1808,1828,1847,1861,1867,1871,1884,1889,1894,1899,1904,1909,1914,1919,1924,1929,1934,1939,1944,1949,1954,1959,1964,1969,1974,1979,1984,1989,1994,1999,2004,2009,2014,2019,2024,2029,2034,2039,2045,2051,2056,2061,2079,2096,2118,2131,2138,2143,2155,2161,2167,2173,2179,2185,2191,2197,2203,2209,2215,2221,2227,2233,2239,2245,2251,2257,2263,2269,2275,2281,2287,2293,2299,2305,2310,2316,2322,2328,2334,2340,2346,2352,2358,2364,2370,2376,2381],{"__ignoreMap":142},[146,1767,1768,1770,1772,1774,1776,1778,1780,1782,1784],{"class":148,"line":149},[146,1769,1028],{"class":1027},[146,1771,1031],{"class":152},[146,1773,1034],{"class":1027},[146,1775,1037],{"class":866},[146,1777,1040],{"class":1027},[146,1779,1043],{"class":152},[146,1781,1046],{"class":175},[146,1783,1049],{"class":152},[146,1785,1052],{"class":152},[146,1787,1788],{"class":148,"line":156},[146,1789,1082],{"emptyLinePlaceholder":936},[146,1791,1792,1794,1797,1799,1801,1803,1805],{"class":148,"line":184},[146,1793,1087],{"class":162},[146,1795,1796],{"class":866}," nonOver ",[146,1798,1093],{"class":152},[146,1800,1263],{"class":866},[146,1802,1176],{"class":152},[146,1804,1357],{"class":1268},[146,1806,1807],{"class":866},"(\n",[146,1809,1810,1813,1816,1818,1821,1823,1825],{"class":148,"line":199},[146,1811,1812],{"class":866},"    [",[146,1814,1815],{"class":1161},"139.6503",[146,1817,1099],{"class":152},[146,1819,1820],{"class":1161}," 35.6762",[146,1822,1221],{"class":866},[146,1824,1099],{"class":152},[146,1826,1827],{"class":1141},"  \u002F\u002F 東京の座標\n",[146,1829,1830,1832,1835,1837,1840,1842,1844],{"class":148,"line":205},[146,1831,1812],{"class":866},[146,1833,1834],{"class":1161},"77.2090",[146,1836,1099],{"class":152},[146,1838,1839],{"class":1161}," 28.6139",[146,1841,1221],{"class":866},[146,1843,1099],{"class":152},[146,1845,1846],{"class":1141}," \u002F\u002F デリーの座標\n",[146,1848,1849,1852,1854,1856,1858],{"class":148,"line":228},[146,1850,1851],{"class":152},"    {",[146,1853,1374],{"class":1214},[146,1855,169],{"class":152},[146,1857,1379],{"class":1161},[146,1859,1860],{"class":152}," }\n",[146,1862,1863,1865],{"class":148,"line":249},[146,1864,1384],{"class":866},[146,1866,1052],{"class":152},[146,1868,1869],{"class":148,"line":270},[146,1870,1082],{"emptyLinePlaceholder":936},[146,1872,1873,1876,1878,1881],{"class":148,"line":284},[146,1874,1875],{"class":866},"console",[146,1877,1176],{"class":152},[146,1879,1880],{"class":1268},"log",[146,1882,1883],{"class":866},"(nonOver)\n",[146,1885,1886],{"class":148,"line":296},[146,1887,1888],{"class":1141},"\u002F**\n",[146,1890,1891],{"class":148,"line":302},[146,1892,1893],{"class":1141},"[\n",[146,1895,1896],{"class":148,"line":316},[146,1897,1898],{"class":1141},"  [ 139.6503, 35.67620000000001 ],\n",[146,1900,1901],{"class":148,"line":326},[146,1902,1903],{"class":1141},"  [ 137.45530635075792, 36.00345099838847 ],\n",[146,1905,1906],{"class":148,"line":332},[146,1907,1908],{"class":1141},"  [ 135.24315241142824, 36.29039534292487 ],\n",[146,1910,1911],{"class":148,"line":338},[146,1912,1913],{"class":1141},"  [ 133.01584313143206, 36.53630839532294 ],\n",[146,1915,1916],{"class":148,"line":344},[146,1917,1918],{"class":1141},"  [ 130.77553308674652, 36.74055732953035 ],\n",[146,1920,1921],{"class":148,"line":1396},[146,1922,1923],{"class":1141},"  [ 128.52450866279412, 36.90260805727012 ],\n",[146,1925,1926],{"class":148,"line":1420},[146,1927,1928],{"class":1141},"  [ 126.26516729530344, 37.022031252284606 ],\n",[146,1930,1931],{"class":148,"line":1425},[146,1932,1933],{"class":1141},"  [ 123.99999414238111, 37.09850731283235 ],\n",[146,1935,1936],{"class":148,"line":1431},[146,1937,1938],{"class":1141},"  [ 121.73153667001746, 37.131830125727525 ],\n",[146,1940,1941],{"class":148,"line":1488},[146,1942,1943],{"class":1141},"  [ 119.46237772511748, 37.121909525547366 ],\n",[146,1945,1946],{"class":148,"line":1518},[146,1947,1948],{"class":1141},"  [ 117.19510773852923, 37.0687723782786 ],\n",[146,1950,1951],{"class":148,"line":1527},[146,1952,1953],{"class":1141},"  [ 114.93229674053903, 36.972562257930214 ],\n",[146,1955,1956],{"class":148,"line":1533},[146,1957,1958],{"class":1141},"  [ 112.67646687998554, 36.83353772552399 ],\n",[146,1960,1961],{"class":148,"line":1566},[146,1962,1963],{"class":1141},"  [ 110.43006611487587, 36.65206926027127 ],\n",[146,1965,1966],{"class":148,"line":1592},[146,1967,1968],{"class":1141},"  [ 108.19544368887564, 36.42863493057574 ],\n",[146,1970,1971],{"class":148,"line":1620},[146,1972,1973],{"class":1141},"  [ 105.97482792817907, 36.163814925900674 ],\n",[146,1975,1976],{"class":148,"line":1630},[146,1977,1978],{"class":1141},"  [ 103.7703067926894, 35.858285097980165 ],\n",[146,1980,1981],{"class":148,"line":1636},[146,1982,1983],{"class":1141},"  [ 101.58381150098748, 35.51280968026889 ],\n",[146,1985,1986],{"class":148,"line":1641},[146,1987,1988],{"class":1141},"  [ 99.41710342759748, 35.12823336735064 ],\n",[146,1990,1991],{"class":148,"line":1646},[146,1992,1993],{"class":1141},"  [ 97.27176435078754, 34.705472941209955 ],\n",[146,1995,1996],{"class":148,"line":1658},[146,1997,1998],{"class":1141},"  [ 95.14919001606306, 34.245508629231686 ],\n",[146,2000,2001],{"class":148,"line":1675},[146,2002,2003],{"class":1141},"  [ 93.05058687993017, 33.749375370334334 ],\n",[146,2005,2006],{"class":148,"line":1686},[146,2007,2008],{"class":1141},"  [ 90.97697181428099, 33.21815415185492 ],\n",[146,2010,2011],{"class":148,"line":1696},[146,2012,2013],{"class":1141},"  [ 88.92917448617246, 32.6529635619425 ],\n",[146,2015,2016],{"class":148,"line":1713},[146,2017,2018],{"class":1141},"  [ 86.90784208162192, 32.05495168160128 ],\n",[146,2020,2021],{"class":148,"line":1724},[146,2022,2023],{"class":1141},"  [ 84.91344601479491, 31.42528841842546 ],\n",[146,2025,2026],{"class":148,"line":1730},[146,2027,2028],{"class":1141},"  [ 82.94629025402638, 30.7651583616406 ],\n",[146,2030,2031],{"class":148,"line":1742},[146,2032,2033],{"class":1141},"  [ 81.00652090116452, 30.075754216293156 ],\n",[146,2035,2036],{"class":148,"line":1747},[146,2037,2038],{"class":1141},"  [ 79.09413667798896, 29.358270854084918 ],\n",[146,2040,2042],{"class":148,"line":2041},41,[146,2043,2044],{"class":1141},"  [ 77.209, 28.613900000000005 ]\n",[146,2046,2048],{"class":148,"line":2047},42,[146,2049,2050],{"class":1141},"]\n",[146,2052,2053],{"class":148,"line":4},[146,2054,2055],{"class":1141},"*\u002F\n",[146,2057,2059],{"class":148,"line":2058},44,[146,2060,1082],{"emptyLinePlaceholder":936},[146,2062,2064,2066,2069,2071,2073,2075,2077],{"class":148,"line":2063},45,[146,2065,1087],{"class":162},[146,2067,2068],{"class":866}," over ",[146,2070,1093],{"class":152},[146,2072,1263],{"class":866},[146,2074,1176],{"class":152},[146,2076,1357],{"class":1268},[146,2078,1807],{"class":866},[146,2080,2082,2084,2086,2088,2090,2092,2094],{"class":148,"line":2081},46,[146,2083,1812],{"class":866},[146,2085,1815],{"class":1161},[146,2087,1099],{"class":152},[146,2089,1820],{"class":1161},[146,2091,1221],{"class":866},[146,2093,1099],{"class":152},[146,2095,1827],{"class":1141},[146,2097,2099,2101,2103,2106,2108,2111,2113,2115],{"class":148,"line":2098},47,[146,2100,1812],{"class":866},[146,2102,1182],{"class":152},[146,2104,2105],{"class":1161},"147.7164",[146,2107,1099],{"class":152},[146,2109,2110],{"class":1161}," 64.8378",[146,2112,1221],{"class":866},[146,2114,1099],{"class":152},[146,2116,2117],{"class":1141}," \u002F\u002F アラスカの座標\n",[146,2119,2121,2123,2125,2127,2129],{"class":148,"line":2120},48,[146,2122,1851],{"class":152},[146,2124,1374],{"class":1214},[146,2126,169],{"class":152},[146,2128,1379],{"class":1161},[146,2130,1860],{"class":152},[146,2132,2134,2136],{"class":148,"line":2133},49,[146,2135,1384],{"class":866},[146,2137,1052],{"class":152},[146,2139,2141],{"class":148,"line":2140},50,[146,2142,1082],{"emptyLinePlaceholder":936},[146,2144,2146,2148,2150,2152],{"class":148,"line":2145},51,[146,2147,1875],{"class":866},[146,2149,1176],{"class":152},[146,2151,1880],{"class":1268},[146,2153,2154],{"class":866},"(over)\n",[146,2156,2158],{"class":148,"line":2157},52,[146,2159,2160],{"class":1141},"\u002F*\n",[146,2162,2164],{"class":148,"line":2163},53,[146,2165,2166],{"class":1141},"  [\n",[146,2168,2170],{"class":148,"line":2169},54,[146,2171,2172],{"class":1141},"    [ 139.6503, 35.67620000000001 ],\n",[146,2174,2176],{"class":148,"line":2175},55,[146,2177,2178],{"class":1141},"    [ 140.80178756980726, 37.16607636013253 ],\n",[146,2180,2182],{"class":148,"line":2181},56,[146,2183,2184],{"class":1141},"    [ 141.99941083018345, 38.64436492965305 ],\n",[146,2186,2188],{"class":148,"line":2187},57,[146,2189,2190],{"class":1141},"    [ 143.24727313996488, 40.10993078543407 ],\n",[146,2192,2194],{"class":148,"line":2193},58,[146,2195,2196],{"class":1141},"    [ 144.54983478842868, 41.56151748104379 ],\n",[146,2198,2200],{"class":148,"line":2199},59,[146,2201,2202],{"class":1141},"    [ 145.91194328157903, 42.9977313513525 ],\n",[146,2204,2206],{"class":148,"line":2205},60,[146,2207,2208],{"class":1141},"    [ 147.33886324769875, 44.41702385324905 ],\n",[146,2210,2212],{"class":148,"line":2211},61,[146,2213,2214],{"class":1141},"    [ 148.83630454782175, 45.81767177569259 ],\n",[146,2216,2218],{"class":148,"line":2217},62,[146,2219,2220],{"class":1141},"    [ 150.41044655311683, 47.19775518537249 ],\n",[146,2222,2224],{"class":148,"line":2223},63,[146,2225,2226],{"class":1141},"    [ 152.0679557258351, 48.55513303641521 ],\n",[146,2228,2230],{"class":148,"line":2229},64,[146,2231,2232],{"class":1141},"    [ 153.8159925691613, 49.88741647693477 ],\n",[146,2234,2236],{"class":148,"line":2235},65,[146,2237,2238],{"class":1141},"    [ 155.66220265313686, 51.19194004906208 ],\n",[146,2240,2242],{"class":148,"line":2241},66,[146,2243,2244],{"class":1141},"    [ 157.6146847535422, 52.465731224424154 ],\n",[146,2246,2248],{"class":148,"line":2247},67,[146,2249,2250],{"class":1141},"    [ 159.68192717003024, 53.705479070518045 ],\n",[146,2252,2254],{"class":148,"line":2253},68,[146,2255,2256],{"class":1141},"    [ 161.87270110244128, 54.90750333482882 ],\n",[146,2258,2260],{"class":148,"line":2259},69,[146,2261,2262],{"class":1141},"    [ 164.19589776721827, 56.06772589186521 ],\n",[146,2264,2266],{"class":148,"line":2265},70,[146,2267,2268],{"class":1141},"    [ 166.66029413034695, 57.18164734362052 ],\n",[146,2270,2272],{"class":148,"line":2271},71,[146,2273,2274],{"class":1141},"    [ 169.27423139783608, 58.24433259326387 ],\n",[146,2276,2278],{"class":148,"line":2277},72,[146,2279,2280],{"class":1141},"    [ 172.0451917704491, 59.25041037671838 ],\n",[146,2282,2284],{"class":148,"line":2283},73,[146,2285,2286],{"class":1141},"    [ 174.9792638386216, 60.19409291290832 ],\n",[146,2288,2290],{"class":148,"line":2289},74,[146,2291,2292],{"class":1141},"    [ 178.08049701800238, 61.069222785803966 ],\n",[146,2294,2296],{"class":148,"line":2295},75,[146,2297,2298],{"class":1141},"    [ 180, 61.53895140096846 ]\n",[146,2300,2302],{"class":148,"line":2301},76,[146,2303,2304],{"class":1141},"  ],\n",[146,2306,2308],{"class":148,"line":2307},77,[146,2309,2166],{"class":1141},[146,2311,2313],{"class":148,"line":2312},78,[146,2314,2315],{"class":1141},"    [ -180, 61.53895140096846 ],\n",[146,2317,2319],{"class":148,"line":2318},79,[146,2320,2321],{"class":1141},"    [ -178.64983787529644, 61.86935452669037 ],\n",[146,2323,2325],{"class":148,"line":2324},80,[146,2326,2327],{"class":1141},"    [ -175.2140408098754, 62.587877615485425 ],\n",[146,2329,2331],{"class":148,"line":2330},81,[146,2332,2333],{"class":1141},"    [ -171.618756489847, 63.21818519285117 ],\n",[146,2335,2337],{"class":148,"line":2336},82,[146,2338,2339],{"class":1141},"    [ -167.87562800253605, 63.753888204781546 ],\n",[146,2341,2343],{"class":148,"line":2342},83,[146,2344,2345],{"class":1141},"    [ -164.0017045661598, 64.18906791167207 ],\n",[146,2347,2349],{"class":148,"line":2348},84,[146,2350,2351],{"class":1141},"    [ -160.0194735821535, 64.51855132598804 ],\n",[146,2353,2355],{"class":148,"line":2354},85,[146,2356,2357],{"class":1141},"    [ -155.9564136218279, 64.73818576863974 ],\n",[146,2359,2361],{"class":148,"line":2360},86,[146,2362,2363],{"class":1141},"    [ -151.84402691822368, 64.84508272020929 ],\n",[146,2365,2367],{"class":148,"line":2366},87,[146,2368,2369],{"class":1141},"    [ -147.7164, 64.8378 ]\n",[146,2371,2373],{"class":148,"line":2372},88,[146,2374,2375],{"class":1141},"  ]\n",[146,2377,2379],{"class":148,"line":2378},89,[146,2380,2050],{"class":1141},[146,2382,2384],{"class":148,"line":2383},90,[146,2385,2055],{"class":1141},[908,2387,2388],{},"html pre.shiki code .s6cf3, html code.shiki .s6cf3{--shiki-default:#89DDFF;--shiki-default-font-style:italic}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .s7ZW3, html code.shiki .s7ZW3{--shiki-default:#BABED8;--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":142,"searchDepth":184,"depth":184,"links":2390},[2391,2392,2393],{"id":957,"depth":156,"text":958},{"id":986,"depth":156,"text":986},{"id":1004,"depth":156,"text":1005,"children":2394},[2395],{"id":1011,"depth":184,"text":1012},[930],"2024-09-22","平面の地図にまっすぐひいた線は最短の航路ではありません。",{},"\u002Farticles\u002F2024_09_22_line_great_circle_antimeridian_cutting",{"title":946,"description":2398},"articles\u002F2024_09_22_line_great_circle_antimeridian_cutting",[1763,2404],"gis","2024_09_22_line_great_circle_antimeridian_cutting\u002Fthumbnail.webp","rjpze4IQqIg5qQZw4X3RE4NP18BGM-IbRT3T9c54h4c",{"id":2408,"title":2409,"body":2410,"category":3217,"createdAt":3218,"description":3219,"extension":933,"index":934,"meta":3220,"navigation":936,"path":3221,"publish":936,"seo":3222,"series":934,"seriesTitle":934,"stem":3223,"tag":3224,"thumbnail":3226,"updatedAt":934,"__hash__":3227},"articles\u002Farticles\u002F2024_08_31_map_lib_price_guide.md","Webやネイティブアプリで地図表示・操作をしたいときのライブラリ選定、使用で考えたいこと",{"type":10,"value":2411,"toc":3183},[2412,2421,2424,2438,2441,2444,2447,2450,2453,2456,2459,2462,2466,2469,2472,2475,2478,2481,2484,2487,2490,2493,2496,2499,2502,2505,2508,2511,2515,2518,2521,2525,2528,2531,2534,2537,2541,2544,2547,2550,2553,2557,2560,2563,2566,2569,2572,2576,2579,2582,2585,2588,2592,2595,2598,2601,2604,2608,2611,2614,2617,2620,2624,2627,2630,2633,2636,2639,2642,2646,2649,3021,3025,3028,3047,3051,3054,3057,3060,3064,3067,3113,3117,3120,3124,3127,3152,3155,3158,3161,3164,3180],[13,2413,2414,2415,2420],{},"こんにちはjunです。私は個人開発で",[53,2416,2419],{"href":2417,"rel":2418},"https:\u002F\u002Froute-share.net",[57],"RouteShare","とよばれる地図を用いた地理情報・アクティビティ共有型webサービスを運営・開発しています。そのサービスではGoogle Map APIを使用して地図を表示したり、ユーザーが任意にピンをおいたりラインを引いたり、地図上から検索できるといった機能を持たせています。このように地図を表示したり、何かしら地図上で編集する機能などをサービスに加えたい時にGoogle Map APIやMapbxのSDKを使用することが多いと思います。しかし課金の体系がわからなかったり、意外と価格がすることもあります。",[13,2422,2423],{},"今回の記事では",[17,2425,2426,2429,2432,2435],{},[20,2427,2428],{},"地図ライブラリに必要なこと",[20,2430,2431],{},"いかにしてwebなどで地図を表示するのか",[20,2433,2434],{},"最低限の構成で低コストで運用する方法",[20,2436,2437],{},"メジャーライブラリの課金体系",[13,2439,2440],{},"という４点について詳細な内容をお伝えしたいとおもいます。主にこれから地図を利用したアプリケーションを開発、運営したいと思う技術者向けの内容です。",[46,2442,2443],{"id":2443},"地図を表示して操作するということ",[13,2445,2446],{},"Google MapやMapboxなどの地図ライブラリを使用して地図を表示し、操作できることは当たり前のように感じるかもしれませんが、その裏側には複雑な仕組みが存在します。ここでは、その仕組みをわかりやすく解説します。",[419,2448,2449],{"id":2449},"様々な操作に応じて指定した地図を表示する",[13,2451,2452],{},"地図アプリを使う際、ユーザーはズームイン・ズームアウト、パン（スワイプ）などの操作を行います。この時、地図ライブラリはその操作に応じて、表示する地図データを動的に更新します。例えば、ズームインすると、現在の中央緯度経度を基点に、より詳細な地図データを読み込み、表示範囲を狭めることで、より詳細な情報を表示します。",[419,2454,2455],{"id":2455},"地図を表示するためにはタイル画像を読み込んでいる",[13,2457,2458],{},"地図データは大きな一枚の画像ではなく、「タイル」と呼ばれる小さな画像に分割されています。これにより、地図を表示する際には、ユーザーの画面に表示される部分だけを効率的に読み込むことができます。例えば、ズームインしたり、パンしたりするたびに、必要な範囲のタイル画像が新たに読み込まれ、表示されます。Googleのネットワークを見てみるとこのようにタイルごとの画像をリクエストされているのがわかります。",[74,2460],{":src":2461,":width":77},"'2024_08_31_map_lib_price_guide\u002Fmaptitle_req.png'",[419,2463,2465],{"id":2464},"どこを表示しているのかをjsなどで制御し必要なタイルをリクエストする","どこを表示しているのかをJSなどで制御し、必要なタイルをリクエストする",[13,2467,2468],{},"地図ライブラリはJavaScriptなどを使って、現在の表示範囲やズームレベルを管理し、それに基づいて必要なタイル画像をサーバーにリクエストします。これにより、無駄なデータの読み込みを避け、効率的に地図を表示することができます。例えば、ユーザーが画面を移動させた際、表示範囲が変わったことを検知し、その範囲に対応するタイル画像を新たに取得して表示します。",[419,2470,2471],{"id":2471},"球体の地球を平面で問題なく表示できるようにするための工夫",[13,2473,2474],{},"地球は球体ですが、地図は平面に表示されます。この変換には「メルカトル図法」などの投影法が使われています。メルカトル図法は、経緯度を一定の間隔で平面に投影することで、球体の地球を平面の地図として表示することができます。ただし、この方法では緯度が高いほど形が歪むため、特に高緯度地方では地図のスケールが大きくなります。",[419,2476,2477],{"id":2477},"地図をスクラッチで開発する場合の考慮点",[13,2479,2480],{},"もし、地図表示機能をスクラッチで開発しようと考えると、いくつか非常に重要な技術や知識が必要となります。まず、地球は球体であるため、それを平面の地図として正確に表現するためには、地理計算技術や地学に関する深い知識が不可欠です。例えば、メルカトル図法などの投影法を用いて、経緯度を正確に平面に変換する必要があります。",[13,2482,2483],{},"さらに、ユーザーがズームイン・ズームアウト、パンなどの操作を行うたびに、その表示位置を正確に計算し、適切な地図データを表示するためのアルゴリズムが求められます。このような技術を実装するには、数学的な計算だけでなく、地理情報システム（GIS）に関する知識も必要です。",[13,2485,2486],{},"また、実際に地図を表示するためには、膨大な数のタイル画像を効率的に管理し、提供するためのタイルサーバも不可欠です。これを自前で構築するとなると、莫大なデータ容量を持つサーバと、それを高速に処理・配信するためのインフラが必要となります。例えば、地球全体をカバーするタイル画像をすべて用意した場合、その総データサイズは数百GBから数TBに達することがあります。",[13,2488,2489],{},"タイルサーバの必要枚数の計算\n地球全体をカバーするためのタイル画像の総枚数は、ズームレベルに依存します。タイルの総枚数は、ズームレベルz に対して次の式で計算されます：",[13,2491,2492],{},"タイル枚数 = 2^(2z)",[13,2494,2495],{},"例えば、ズームレベル0では1枚、ズームレベル1では4枚、ズームレベル2では16枚のタイルが必要です。ズームレベルが上がるごとに、必要なタイル枚数は指数関数的に増加します。ズームレベル10の場合、約1,048,576枚、ズームレベル15では約1,073,741,824枚ものタイルが必要となります。まして言語ごとに表示を変えるとならにさらに必要となります。",[13,2497,2498],{},"これらを保存し、ユーザーに迅速に提供するためには、高性能かつ大容量のストレージが必要となり、その運用コストも無視できません。静的でないサーバーサイドレンダリングでもサーバーパワーとキャッシュを工夫する知識が必要です。",[13,2500,2501],{},"特に、Web上で地図を描画する際には、CanvasやWebGLなどの描画技術が使われます。これにより、地図データをピクセル単位で描画し、ユーザーの操作に応じてリアルタイムで更新することが可能です。しかし、このような技術を一から実装することは非常に手間がかかり、パフォーマンスの最適化やブラウザ間の互換性を考慮する必要があります。",[13,2503,2504],{},"さらに、Google Mapを例にとると、短く・狭い範囲での２点は直線ですが広い範囲では曲線になります。実際は球面に沿ったまっすぐな線を引いていますが、それを平面に表した結果です。",[74,2506],{":src":2507,":width":77},"'2024_08_31_map_lib_price_guide\u002Fdistorted_map_example.png'",[13,2509,2510],{},"このように、広範囲での地図表示には特有の課題があり、それらを解決するための技術が必要となります。",[46,2512,2514],{"id":2513},"メジャーライブラリタイルサーバの仕組みと課金体系","メジャーライブラリ・タイルサーバの仕組みと課金体系",[13,2516,2517],{},"地図表示ライブラリやタイルサーバは、地図アプリケーションの開発において重要な要素です。ここでは、代表的なライブラリとタイルサーバの紹介とともに、それぞれのメリット、デメリット、制限事項、そして課金体系について解説します。なお2024年8月現在の内容です。",[419,2519,2520],{"id":2520},"地図表示ライブラリ",[819,2522,2524],{"id":2523},"google-map-sdk","Google Map SDK",[13,2526,2527],{},"メリット:世界中で広く使用され、信頼性が高い。多機能で詳細な地図データやストリートビューなど豊富なAPIを提供。ドキュメントが充実し、コミュニティサポートも豊富。SDK以外にも地理計算ライブラリが含まれており、高度なカスタマイズが可能。",[13,2529,2530],{},"デメリット:課金体系が比較的高額で、使用量が増えるとコストが急増する可能性がある。特定のデータや機能に制約がある場合がある。",[13,2532,2533],{},"制限事項:無料使用枠があるが、一定のリクエスト数を超えると課金が発生する。Googleの著作表示が必要。",[13,2535,2536],{},"課金体系:web・ネイティブ共に月間のSDKリクエスト数に基づく従量課金制。SDKのインスタンス作成時に課金され、料金はAPIごとに異なり、使用する機能によって変動する。",[819,2538,2540],{"id":2539},"mapbox-sdk","Mapbox SDK",[13,2542,2543],{},"メリット:新興のプラットフォームで、Google Mapよりオープンかつデベロッパーフレンドリー。高度にカスタマイズ可能で、地図のデザインやデータ表示を柔軟にコントロールできる。料金体系がGoogle Mapより安価で、小規模なプロジェクトやスタートアップに適している。",[13,2545,2546],{},"デメリット:Google Mapに比べてコミュニティ規模が小さく、サポートリソースが限られる場合がある。人気が出た場合、Google Mapに近い価格になる可能性がある。",[13,2548,2549],{},"制限事項:商用利用時に無料枠を超えると従量課金が発生する。Mapboxの著作表示が必要。",[13,2551,2552],{},"課金体系:web版はSDKのインスタンス作成時をSDKリクエストとして課金、ネイティブ版はアクセストークンから計測されるアクティブユーザー数に基づく。",[819,2554,2556],{"id":2555},"leafletjs","Leaflet.js",[13,2558,2559],{},"メリット:軽量でシンプルなオープンソースライブラリでカスタマイズ性が高い。大規模な地図アプリケーションだけでなく、軽量な地図表示にも適しており、無料で使用可能。",[13,2561,2562],{},"デメリット:標準機能は少なく、複雑な機能や高度なカスタマイズには他のプラグインやライブラリが必要。自前でタイルサーバを用意する必要がある場合がある。",[13,2564,2565],{},"制限事項:オープンソースのため、サポートはコミュニティに依存する部分が大きい。",[13,2567,2568],{},"課金体系:Leaflet.js自体は無料。ただし、タイルサーバ利用には別途コストが発生する場合がある。",[419,2570,2571],{"id":2571},"タイルサーバ",[819,2573,2575],{"id":2574},"maptiler","MapTiler",[13,2577,2578],{},"メリット:高品質な地図タイルを提供し、世界中の地図データにアクセス可能。カスタマイズが容易で商用利用もサポート。",[13,2580,2581],{},"デメリット:高度な機能や大規模な使用にはそれ相応のコストがかかる。",[13,2583,2584],{},"制限事項:商用利用には有料プランが必要。",[13,2586,2587],{},"課金体系:利用量に応じた従量課金制で、使用するタイル数やアクセス頻度に基づいて料金が決まる。",[819,2589,2591],{"id":2590},"openstreetmap","OpenStreetMap",[13,2593,2594],{},"メリット:オープンソースプロジェクトであり、無料で利用可能。世界中のユーザーがデータを提供し、地図データが豊富。主にテスト時や地図を印刷する際に使用される。",[13,2596,2597],{},"デメリット:商用利用は基本的に不可能で使用には制限がある。データの正確性や一貫性にバラつきがある場合がある。アクセスしすぎるとブロックされる。",[13,2599,2600],{},"制限事項:大規模な商用プロジェクトでの利用は推奨されない。",[13,2602,2603],{},"課金体系:無料で使用可能だが、商用利用時には著作表示が必要。",[819,2605,2607],{"id":2606},"mapbox-tile-server","Mapbox Tile Server",[13,2609,2610],{},"メリット:高品質な地図タイルを提供し、Mapbox SDKとの統合が容易。高度にカスタマイズ可能で、デザインや機能を自由に変更できる。",[13,2612,2613],{},"デメリット:使用量に応じてコストがかかるため、大規模なプロジェクトでは費用が高くなる可能性がある。",[13,2615,2616],{},"制限事項:無料使用枠があるが、商用利用には有料プランが必要。",[13,2618,2619],{},"課金体系:使用するタイル数やユーザー数に応じた従量課金制。",[819,2621,2623],{"id":2622},"google-map-tile","Google Map Tile",[13,2625,2626],{},"メリット:信頼性が高く、Googleの地図データを利用可能。多機能で、他のGoogleサービスとの連携が容易。",[13,2628,2629],{},"デメリット:料金が高めで、使用量が増えるとコストが急増する可能性がある。",[13,2631,2632],{},"制限事項:一定の無料枠を超えると課金が発生する。",[13,2634,2635],{},"課金体系:タイルのリクエスト数に基づいた従量課金制。",[46,2637,2638],{"id":2638},"webでの最小の構成",[13,2640,2641],{},"Webアプリケーションで地図を表示する際、コストを抑えつつシンプルに構成するためには、軽量な地図ライブラリと信頼性の高いタイルサーバを選ぶことが重要です。ここでは、Leaflet.jsとMapboxのタイルサーバを使用した最小構成について説明します。",[419,2643,2645],{"id":2644},"_1-leafletjsの導入","1. Leaflet.jsの導入",[13,2647,2648],{},"Leaflet.jsは、軽量でシンプルなオープンソースのJavaScriptライブラリです。基本的な地図表示機能を備えており、軽量でありながら高いカスタマイズ性を持っています。まずは、Leaflet.jsをプロジェクトに導入します。",[137,2650,2654],{"className":2651,"code":2652,"language":2653,"meta":142,"style":142},"language-html shiki shiki-themes material-theme-ocean","\u003C!DOCTYPE html>\n\u003Chtml>\n\u003Chead>\n    \u003Ctitle>Leaflet Map\u003C\u002Ftitle>\n    \u003Clink rel=\"stylesheet\" href=\"https:\u002F\u002Funpkg.com\u002Fleaflet@1.7.1\u002Fdist\u002Fleaflet.css\" \u002F>\n    \u003Cscript src=\"https:\u002F\u002Funpkg.com\u002Fleaflet@1.7.1\u002Fdist\u002Fleaflet.js\">\u003C\u002Fscript>\n\u003C\u002Fhead>\n\u003Cbody>\n    \u003Cdiv id=\"map\" style=\"width: 600px; height: 400px;\">\u003C\u002Fdiv>\n    \u003Cscript>\n        var map = L.map('map').setView([51.505, -0.09], 13);\n\n        L.tileLayer('https:\u002F\u002Fapi.mapbox.com\u002Fstyles\u002Fv1\u002F{id}\u002Ftiles\u002F{z}\u002F{x}\u002F{y}?access_token=YOUR_MAPBOX_ACCESS_TOKEN', {\n            maxZoom: 18,\n            id: 'mapbox\u002Fstreets-v11',\n            tileSize: 512,\n            zoomOffset: -1,\n            accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN'\n        }).addTo(map);\n    \u003C\u002Fscript>\n\u003C\u002Fbody>\n\u003C\u002Fhtml>\n","html",[102,2655,2656,2670,2678,2687,2708,2742,2768,2776,2785,2821,2829,2885,2889,2912,2924,2940,2952,2965,2980,2996,3005,3013],{"__ignoreMap":142},[146,2657,2658,2661,2664,2667],{"class":148,"line":149},[146,2659,2660],{"class":152},"\u003C!",[146,2662,2663],{"class":1214},"DOCTYPE",[146,2665,2666],{"class":162}," html",[146,2668,2669],{"class":152},">\n",[146,2671,2672,2674,2676],{"class":148,"line":156},[146,2673,1170],{"class":152},[146,2675,2653],{"class":1214},[146,2677,2669],{"class":152},[146,2679,2680,2682,2685],{"class":148,"line":184},[146,2681,1170],{"class":152},[146,2683,2684],{"class":1214},"head",[146,2686,2669],{"class":152},[146,2688,2689,2692,2695,2698,2701,2704,2706],{"class":148,"line":199},[146,2690,2691],{"class":152},"    \u003C",[146,2693,2694],{"class":1214},"title",[146,2696,2697],{"class":152},">",[146,2699,2700],{"class":866},"Leaflet Map",[146,2702,2703],{"class":152},"\u003C\u002F",[146,2705,2694],{"class":1214},[146,2707,2669],{"class":152},[146,2709,2710,2712,2715,2718,2720,2722,2725,2727,2730,2732,2734,2737,2739],{"class":148,"line":205},[146,2711,2691],{"class":152},[146,2713,2714],{"class":1214},"link",[146,2716,2717],{"class":162}," rel",[146,2719,1093],{"class":152},[146,2721,166],{"class":152},[146,2723,2724],{"class":175},"stylesheet",[146,2726,166],{"class":152},[146,2728,2729],{"class":162}," href",[146,2731,1093],{"class":152},[146,2733,166],{"class":152},[146,2735,2736],{"class":175},"https:\u002F\u002Funpkg.com\u002Fleaflet@1.7.1\u002Fdist\u002Fleaflet.css",[146,2738,166],{"class":152},[146,2740,2741],{"class":152}," \u002F>\n",[146,2743,2744,2746,2749,2752,2754,2756,2759,2761,2764,2766],{"class":148,"line":228},[146,2745,2691],{"class":152},[146,2747,2748],{"class":1214},"script",[146,2750,2751],{"class":162}," src",[146,2753,1093],{"class":152},[146,2755,166],{"class":152},[146,2757,2758],{"class":175},"https:\u002F\u002Funpkg.com\u002Fleaflet@1.7.1\u002Fdist\u002Fleaflet.js",[146,2760,166],{"class":152},[146,2762,2763],{"class":152},">\u003C\u002F",[146,2765,2748],{"class":1214},[146,2767,2669],{"class":152},[146,2769,2770,2772,2774],{"class":148,"line":249},[146,2771,2703],{"class":152},[146,2773,2684],{"class":1214},[146,2775,2669],{"class":152},[146,2777,2778,2780,2783],{"class":148,"line":270},[146,2779,1170],{"class":152},[146,2781,2782],{"class":1214},"body",[146,2784,2669],{"class":152},[146,2786,2787,2789,2791,2794,2796,2798,2801,2803,2806,2808,2810,2813,2815,2817,2819],{"class":148,"line":284},[146,2788,2691],{"class":152},[146,2790,39],{"class":1214},[146,2792,2793],{"class":162}," id",[146,2795,1093],{"class":152},[146,2797,166],{"class":152},[146,2799,2800],{"class":175},"map",[146,2802,166],{"class":152},[146,2804,2805],{"class":162}," style",[146,2807,1093],{"class":152},[146,2809,166],{"class":152},[146,2811,2812],{"class":175},"width: 600px; height: 400px;",[146,2814,166],{"class":152},[146,2816,2763],{"class":152},[146,2818,39],{"class":1214},[146,2820,2669],{"class":152},[146,2822,2823,2825,2827],{"class":148,"line":296},[146,2824,2691],{"class":152},[146,2826,2748],{"class":1214},[146,2828,2669],{"class":152},[146,2830,2831,2834,2837,2839,2842,2844,2846,2848,2850,2852,2854,2856,2858,2861,2863,2866,2868,2871,2874,2876,2878,2881,2883],{"class":148,"line":302},[146,2832,2833],{"class":162},"        var",[146,2835,2836],{"class":866}," map ",[146,2838,1093],{"class":152},[146,2840,2841],{"class":866}," L",[146,2843,1176],{"class":152},[146,2845,2800],{"class":1268},[146,2847,1360],{"class":866},[146,2849,1049],{"class":152},[146,2851,2800],{"class":175},[146,2853,1049],{"class":152},[146,2855,1384],{"class":866},[146,2857,1176],{"class":152},[146,2859,2860],{"class":1268},"setView",[146,2862,1272],{"class":866},[146,2864,2865],{"class":1161},"51.505",[146,2867,1099],{"class":152},[146,2869,2870],{"class":152}," -",[146,2872,2873],{"class":1161},"0.09",[146,2875,1221],{"class":866},[146,2877,1099],{"class":152},[146,2879,2880],{"class":1161}," 13",[146,2882,1384],{"class":866},[146,2884,1052],{"class":152},[146,2886,2887],{"class":148,"line":316},[146,2888,1082],{"emptyLinePlaceholder":936},[146,2890,2891,2894,2896,2899,2901,2903,2906,2908,2910],{"class":148,"line":326},[146,2892,2893],{"class":866},"        L",[146,2895,1176],{"class":152},[146,2897,2898],{"class":1268},"tileLayer",[146,2900,1360],{"class":866},[146,2902,1049],{"class":152},[146,2904,2905],{"class":175},"https:\u002F\u002Fapi.mapbox.com\u002Fstyles\u002Fv1\u002F{id}\u002Ftiles\u002F{z}\u002F{x}\u002F{y}?access_token=YOUR_MAPBOX_ACCESS_TOKEN",[146,2907,1049],{"class":152},[146,2909,1099],{"class":152},[146,2911,861],{"class":152},[146,2913,2914,2917,2919,2922],{"class":148,"line":332},[146,2915,2916],{"class":1214},"            maxZoom",[146,2918,169],{"class":152},[146,2920,2921],{"class":1161}," 18",[146,2923,181],{"class":152},[146,2925,2926,2929,2931,2933,2936,2938],{"class":148,"line":338},[146,2927,2928],{"class":1214},"            id",[146,2930,169],{"class":152},[146,2932,1043],{"class":152},[146,2934,2935],{"class":175},"mapbox\u002Fstreets-v11",[146,2937,1049],{"class":152},[146,2939,181],{"class":152},[146,2941,2942,2945,2947,2950],{"class":148,"line":344},[146,2943,2944],{"class":1214},"            tileSize",[146,2946,169],{"class":152},[146,2948,2949],{"class":1161}," 512",[146,2951,181],{"class":152},[146,2953,2954,2957,2959,2961,2963],{"class":148,"line":1396},[146,2955,2956],{"class":1214},"            zoomOffset",[146,2958,169],{"class":152},[146,2960,2870],{"class":152},[146,2962,1280],{"class":1161},[146,2964,181],{"class":152},[146,2966,2967,2970,2972,2974,2977],{"class":148,"line":1420},[146,2968,2969],{"class":1214},"            accessToken",[146,2971,169],{"class":152},[146,2973,1043],{"class":152},[146,2975,2976],{"class":175},"YOUR_MAPBOX_ACCESS_TOKEN",[146,2978,2979],{"class":152},"'\n",[146,2981,2982,2984,2986,2988,2991,2994],{"class":148,"line":1425},[146,2983,1623],{"class":152},[146,2985,1384],{"class":866},[146,2987,1176],{"class":152},[146,2989,2990],{"class":1268},"addTo",[146,2992,2993],{"class":866},"(map)",[146,2995,1052],{"class":152},[146,2997,2998,3001,3003],{"class":148,"line":1431},[146,2999,3000],{"class":152},"    \u003C\u002F",[146,3002,2748],{"class":1214},[146,3004,2669],{"class":152},[146,3006,3007,3009,3011],{"class":148,"line":1488},[146,3008,2703],{"class":152},[146,3010,2782],{"class":1214},[146,3012,2669],{"class":152},[146,3014,3015,3017,3019],{"class":148,"line":1518},[146,3016,2703],{"class":152},[146,3018,2653],{"class":1214},[146,3020,2669],{"class":152},[419,3022,3024],{"id":3023},"_2-mapboxタイルサーバの契約と設定","2. Mapboxタイルサーバの契約と設定",[13,3026,3027],{},"Leaflet.js自体は無料で利用できますが、タイル画像を提供するサーバが必要です。ここでは、Mapboxのタイルサーバを契約します。Mapboxは信頼性が高く、カスタマイズ性に優れたタイルサーバを提供しています。また無料枠内であればタイルも無料で利用できます。",[398,3029,3030,3038,3044],{},[20,3031,3032,3037],{},[53,3033,3036],{"href":3034,"rel":3035},"https:\u002F\u002Fwww.mapbox.com\u002F",[57],"Mapboxの公式サイト","でアカウントを作成します。",[20,3039,3040,3041,3043],{},"タイルを作成し、アクセストークンを取得し、上記のLeaflet.jsのコードにある ",[102,3042,2976],{}," の部分に取得したアクセストークンを入力します。",[20,3045,3046],{},"必要に応じて、地図のスタイルやズームレベルをカスタマイズできます。",[419,3048,3050],{"id":3049},"_3-最小構成のメリットと注意点","3. 最小構成のメリットと注意点",[13,3052,3053],{},"この最小構成では、Leaflet.jsの軽量なコードとMapboxの高品質なタイルサーバを組み合わせて、低コストでシンプルな地図表示を実現できます。ただし、商用利用の場合は、Mapboxの料金体系に注意し、利用量に応じたプランを選択する必要があります。\nまた、シンプルな構成であるため、複雑なカスタマイズや高負荷のユースケースには向いていない場合があります。プロジェクトの規模や要件に応じて、適切なライブラリやサーバ構成を検討することが重要です。",[46,3055,3056],{"id":3056},"部分的な仕様の場合",[13,3058,3059],{},"特定の地域や用途に限定した地図表示を行う場合、全世界をカバーする必要がないため、タイルアクセスを最適化することが可能です。ここでは、部分的な仕様における地図表示の方法について説明します。",[419,3061,3063],{"id":3062},"_1-エリア制限によるタイルアクセスの節約","1. エリア制限によるタイルアクセスの節約",[13,3065,3066],{},"特定の地域のみを対象とする場合、表示できるエリアを緯度経度範囲とズームレベルで制限することで、無駄なタイルアクセスを減らし、コストを節約できます。例えば、都市単位での地図表示や、特定の観光地を中心にしたアプリケーションの場合、必要最小限のエリアだけを表示するよう設定することで、効率的なタイルアクセスが可能です。",[137,3068,3072],{"className":3069,"code":3070,"language":3071,"meta":142,"style":142},"language-javascript shiki shiki-themes material-theme-ocean","var map = L.map('map', {\n    center: [35.6895, 139.6917], \u002F\u002F 東京の緯度経度\n    zoom: 12,\n    maxBounds: [\n        [35.0, 138.0], \u002F\u002F 南西端\n        [36.0, 140.0]  \u002F\u002F 北東端\n    ]\n});\n","javascript",[102,3073,3074,3079,3084,3089,3094,3099,3104,3108],{"__ignoreMap":142},[146,3075,3076],{"class":148,"line":149},[146,3077,3078],{},"var map = L.map('map', {\n",[146,3080,3081],{"class":148,"line":156},[146,3082,3083],{},"    center: [35.6895, 139.6917], \u002F\u002F 東京の緯度経度\n",[146,3085,3086],{"class":148,"line":184},[146,3087,3088],{},"    zoom: 12,\n",[146,3090,3091],{"class":148,"line":199},[146,3092,3093],{},"    maxBounds: [\n",[146,3095,3096],{"class":148,"line":205},[146,3097,3098],{},"        [35.0, 138.0], \u002F\u002F 南西端\n",[146,3100,3101],{"class":148,"line":228},[146,3102,3103],{},"        [36.0, 140.0]  \u002F\u002F 北東端\n",[146,3105,3106],{"class":148,"line":249},[146,3107,341],{},[146,3109,3110],{"class":148,"line":270},[146,3111,3112],{},"});\n",[419,3114,3116],{"id":3115},"_2-webでの利用leafletjsの活用","2. Webでの利用：Leaflet.jsの活用",[13,3118,3119],{},"Webアプリケーションにおいて、軽量でカスタマイズ性の高いLeaflet.jsを利用することは有効です。Leaflet.jsは、シンプルな実装で特定エリアの地図表示を行うことができ、タイルサーバへのアクセスを最小限に抑えることができます。",[419,3121,3123],{"id":3122},"_3-日本国内のみの利用国土地理院のタイルサーバ","3. 日本国内のみの利用：国土地理院のタイルサーバ",[13,3125,3126],{},"日本国内のみを対象とする場合、国土地理院が提供するタイルサーバを利用することも一つの選択肢です。国土地理院のタイルは無料で利用可能で、日本の詳細な地図データを提供しています。Leaflet.jsと組み合わせることで、日本国内での特定エリアを効率的に表示することが可能です。",[137,3128,3130],{"className":3069,"code":3129,"language":3071,"meta":142,"style":142},"L.tileLayer('https:\u002F\u002Fcyberjapandata.gsi.go.jp\u002Fxyz\u002Fstd\u002F{z}\u002F{x}\u002F{y}.png', {\n    maxZoom: 18,\n    attribution: '© 国土地理院'\n}).addTo(map);\n",[102,3131,3132,3137,3142,3147],{"__ignoreMap":142},[146,3133,3134],{"class":148,"line":149},[146,3135,3136],{},"L.tileLayer('https:\u002F\u002Fcyberjapandata.gsi.go.jp\u002Fxyz\u002Fstd\u002F{z}\u002F{x}\u002F{y}.png', {\n",[146,3138,3139],{"class":148,"line":156},[146,3140,3141],{},"    maxZoom: 18,\n",[146,3143,3144],{"class":148,"line":184},[146,3145,3146],{},"    attribution: '© 国土地理院'\n",[146,3148,3149],{"class":148,"line":199},[146,3150,3151],{},"}).addTo(map);\n",[13,3153,3154],{},"このように、部分的な仕様の場合には、地図表示エリアを限定することで、コストを抑えつつ必要な情報を提供することが可能です。利用する地域や用途に応じて、最適な構成を選択してください。",[46,3156,3157],{"id":3157},"結論",[13,3159,3160],{},"とりあえず内容はこの通りです。記述した通り、地図表示というのはそれなりのサーバコストやライブラリの開発コストが高いため、ある意味一部のサービスの寡占状態になっています。しかし地図表示は非常にわかりやすく需要が多いためgoogle mapが高くても使わざる得ないような状態になっています。ですが、google map SDK以外にも上記の最小構成などである程度コストを節約することができるので、小さいサービスや自治体が地図を使用したいときなどに利用できます。ちなみにネイティブの場合はSDKに当たるものにleaflet.jsのようなものがないので難しいです。私もreact nativeで探したのですが見当たらない様子です。この辺は調べてまた見つかったら更新しようと思います。",[46,3162,3163],{"id":3163},"参考",[17,3165,3166,3173],{},[20,3167,3168],{},[53,3169,3172],{"href":3170,"rel":3171},"https:\u002F\u002Fspeakerdeck.com\u002Fsmellman\u002Fguo-nei-xiang-ketairusabafalsegou-zhu-toyun-yong-nituite?slide=22",[57],"国内向けタイルサーバの構築と運用について",[20,3174,3175],{},[53,3176,3179],{"href":3177,"rel":3178},"https:\u002F\u002Fwww.gsi.go.jp\u002FGIS\u002Fwhatisgis.html",[57],"GISとは",[908,3181,3182],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}",{"title":142,"searchDepth":184,"depth":184,"links":3184},[3185,3192,3205,3210,3215,3216],{"id":2443,"depth":156,"text":2443,"children":3186},[3187,3188,3189,3190,3191],{"id":2449,"depth":184,"text":2449},{"id":2455,"depth":184,"text":2455},{"id":2464,"depth":184,"text":2465},{"id":2471,"depth":184,"text":2471},{"id":2477,"depth":184,"text":2477},{"id":2513,"depth":156,"text":2514,"children":3193},[3194,3199],{"id":2520,"depth":184,"text":2520,"children":3195},[3196,3197,3198],{"id":2523,"depth":199,"text":2524},{"id":2539,"depth":199,"text":2540},{"id":2555,"depth":199,"text":2556},{"id":2571,"depth":184,"text":2571,"children":3200},[3201,3202,3203,3204],{"id":2574,"depth":199,"text":2575},{"id":2590,"depth":199,"text":2591},{"id":2606,"depth":199,"text":2607},{"id":2622,"depth":199,"text":2623},{"id":2638,"depth":156,"text":2638,"children":3206},[3207,3208,3209],{"id":2644,"depth":184,"text":2645},{"id":3023,"depth":184,"text":3024},{"id":3049,"depth":184,"text":3050},{"id":3056,"depth":156,"text":3056,"children":3211},[3212,3213,3214],{"id":3062,"depth":184,"text":3063},{"id":3115,"depth":184,"text":3116},{"id":3122,"depth":184,"text":3123},{"id":3157,"depth":156,"text":3157},{"id":3163,"depth":156,"text":3163},[930],"2024-08-31","地図アプリで考えておくこと",{},"\u002Farticles\u002F2024_08_31_map_lib_price_guide",{"title":2409,"description":3219},"articles\u002F2024_08_31_map_lib_price_guide",[1763,3225],"native","2024_08_31_map_lib_price_guide\u002Fthumbnail.webp","UCWkKbRmKWP-9IKYE6ttGwipqTvfmV2bZ1WTCC2d2BA",{"id":3229,"title":3230,"body":3231,"category":5191,"createdAt":5192,"description":5193,"extension":933,"index":156,"meta":5194,"navigation":936,"path":5195,"publish":936,"seo":5196,"series":5197,"seriesTitle":5198,"stem":5199,"tag":5200,"thumbnail":5203,"updatedAt":934,"__hash__":5204},"series\u002Fseries\u002Frtmp-manager-server-2.md","配信中継サーバ開発2：ffmpegを用いたyoutubeへのテスト配信とローカルデバッグ環境の構築",{"type":10,"value":3232,"toc":5170},[3233,3236,3239,3242,3253,3256,3259,3262,3278,3281,3284,3292,3295,3337,3357,3627,3630,3887,3889,4098,4105,4252,4255,4622,4625,4628,4631,4634,4727,4754,4757,4760,4767,4773,4779,4790,4794,4797,4801,4804,4815,4818,4821,4824,4830,4836,4840,4847,4853,4865,4868,4871,4874,4878,4881,4889,4893,4896,4899,4902,4905,4910,4913,4916,4919,4922,4925,4928,4931,4934,4945,4948,4951,4954,4957,4961,4964,5098,5108,5114,5120,5123,5126,5132,5139,5142,5145,5148,5151,5167],[13,3234,3235],{},"こんにちはjunです。前回の記事から2年も経ってしまいましたが、サイクリング欲が再度湧いてきたのでこの内容を完成させるように頑張ります。ではとりあえず前回はgoproからdockerのRTMPサーバに送信してローカルマシン上で見れるかを確かめました。最終的には以下のような構成を開発します。（その１のものと変わっています）",[74,3237],{":src":3238,":width":77},"'rtmp-docker-local\u002Ffig2.jpg'",[13,3240,3241],{},"今回の記事は",[17,3243,3244,3247,3250],{},[20,3245,3246],{},"上記図の環境をローカルでdockerで構築する",[20,3248,3249],{},"管理用サーバ上のffmpegを用いてyoutubeにgoproの映像を中継してライブする",[20,3251,3252],{},"ローカルで簡単に配信をデバッグできる環境を構築する\nの3点を解説したいと思います。",[13,3254,3255],{},"これらの内容をまずローカルで実装するためにDockerを用いて構築します。使用しているOSはmacOs Ventura 13.4.1でdockerは4.17.0です。また前回から2年たっているのでdockerfileなどを修正します。",[46,3257,3258],{"id":3258},"完成品",[13,3260,3261],{},"この記事でのゴールはこんな感じです。（字幕ON推奨）",[39,3263,3264],{},[3265,3266,3267,3276],"no-ssr",{},[3268,3269],"iframe",{"width":3270,"height":3271,"src":3272,"title":3273,"frameBorder":1291,"allow":3274,"referrerPolicy":3275,"allowFullScreen":936},"100%","400px","https:\u002F\u002Fwww.youtube.com\u002Fembed\u002FTY61fzShv30?si=a72lxLH1mvLsYTma","YouTube video player","accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share","strict-origin-when-cross-origin",[3265,3277],{},[46,3279,3280],{"id":3280},"全ファイル構成",[13,3282,3283],{},"とりあえず完成した必要なファイルと構成を載せます。",[137,3285,3290],{"className":3286,"code":3288,"language":3289},[3287],"language-text",".\n├── Dockerfile\n├── conf\n│   ├── 000-default.conf\n│   ├── default.conf\n│   └── nginx.conf\n├── debug\n│   └── index.html\n├── docker-compose.yml\n└── src\n    └── public\n        └── test.html\n","text",[102,3291,3288],{"__ignoreMap":142},[13,3293,3294],{},"Dockerfileは管理用コンテナの構成に変更。あとでwebフレームワークのlumenを入れるためにcomposerを入れています。（この回では入れません）。そしてffmpegを入れておきます。",[137,3296,3300],{"className":3297,"code":3298,"language":3299,"meta":142,"style":142},"language-Dockerfile shiki shiki-themes material-theme-ocean","FROM php:apache\nCOPY --from=composer \u002Fusr\u002Fbin\u002Fcomposer \u002Fusr\u002Fbin\u002Fcomposer\nRUN curl -O https:\u002F\u002Fjohnvansickle.com\u002Fffmpeg\u002Freleases\u002Fffmpeg-release-amd64-static.tar.xz && \\\n    tar -xJf ffmpeg-release-amd64-static.tar.xz && \\\n    mv ffmpeg-*\u002Fffmpeg \u002Fusr\u002Flocal\u002Fbin\u002F && \\\n    mv ffmpeg-*\u002Fffprobe \u002Fusr\u002Flocal\u002Fbin\u002F && \\\n    rm -rf ffmpeg-*\n","Dockerfile",[102,3301,3302,3307,3312,3317,3322,3327,3332],{"__ignoreMap":142},[146,3303,3304],{"class":148,"line":149},[146,3305,3306],{},"FROM php:apache\n",[146,3308,3309],{"class":148,"line":156},[146,3310,3311],{},"COPY --from=composer \u002Fusr\u002Fbin\u002Fcomposer \u002Fusr\u002Fbin\u002Fcomposer\n",[146,3313,3314],{"class":148,"line":184},[146,3315,3316],{},"RUN curl -O https:\u002F\u002Fjohnvansickle.com\u002Fffmpeg\u002Freleases\u002Fffmpeg-release-amd64-static.tar.xz && \\\n",[146,3318,3319],{"class":148,"line":199},[146,3320,3321],{},"    tar -xJf ffmpeg-release-amd64-static.tar.xz && \\\n",[146,3323,3324],{"class":148,"line":205},[146,3325,3326],{},"    mv ffmpeg-*\u002Fffmpeg \u002Fusr\u002Flocal\u002Fbin\u002F && \\\n",[146,3328,3329],{"class":148,"line":228},[146,3330,3331],{},"    mv ffmpeg-*\u002Fffprobe \u002Fusr\u002Flocal\u002Fbin\u002F && \\\n",[146,3333,3334],{"class":148,"line":249},[146,3335,3336],{},"    rm -rf ffmpeg-*\n",[17,3338,3339,3345,3351],{},[20,3340,3341,3344],{},[102,3342,3343],{},"rtmptarget","がテスト配信先コンテナ",[20,3346,3347,3350],{},[102,3348,3349],{},"rtmp","がGorpoなどからの受信用コンテナ",[20,3352,3353,3356],{},[102,3354,3355],{},"php","が管理用web uiや配信媒体へ送信する処理を行うコンテナ",[137,3358,3363],{"className":3359,"code":3360,"filename":3361,"language":3362,"meta":142,"style":142},"language-yml shiki shiki-themes material-theme-ocean","version: '2'\nservices:\n  rtmptarget:\n    image: tiangolo\u002Fnginx-rtmp\n    ports:\n      - \"1930:1935\"\n      - \"1931:80\"\n    volumes:\n      - .\u002Fconf\u002Fnginx.conf:\u002Fetc\u002Fnginx\u002Fnginx.conf\n      - .\u002Fconf\u002Fdefault.conf:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf\n      - .\u002Fdebug:\u002Fusr\u002Fshare\u002Fnginx\u002F\n  rtmp:\n    image: tiangolo\u002Fnginx-rtmp\n    ports:\n      - \"1935:1935\"\n    volumes:\n      - shared_data:\u002Fusr\u002Fshare\u002Fnginx\u002Fhls\n      - .\u002Fconf\u002Fnginx.conf:\u002Fetc\u002Fnginx\u002Fnginx.conf\n  php:\n    build: .\n    expose:\n      - 1935\n    ports:\n      - \"8080:80\"\n    volumes:\n      - shared_data:\u002Fvar\u002Fwww\u002Fhtml\u002Fhls\n      - .\u002Fsrc:\u002Fvar\u002Fwww\u002Fhtml\n      - .\u002Fconf\u002F.htpasswd:\u002Fvar\u002Fwww\u002F.htpasswd\n      - .\u002Fconf\u002F000-default.conf:\u002Fetc\u002Fapache2\u002Fsites-enabled\u002F000-default.conf\n    extra_hosts:\n      - \"host.docker.internal:host-gateway\"\nvolumes:\n  shared_data:\n","docker-compose.yml","yml",[102,3364,3365,3379,3387,3394,3404,3411,3423,3434,3441,3448,3455,3462,3469,3477,3483,3494,3500,3507,3513,3520,3530,3537,3544,3550,3561,3567,3574,3581,3588,3595,3602,3613,3620],{"__ignoreMap":142},[146,3366,3367,3370,3372,3374,3377],{"class":148,"line":149},[146,3368,3369],{"class":1214},"version",[146,3371,169],{"class":152},[146,3373,1043],{"class":152},[146,3375,3376],{"class":175},"2",[146,3378,2979],{"class":152},[146,3380,3381,3384],{"class":148,"line":156},[146,3382,3383],{"class":1214},"services",[146,3385,3386],{"class":152},":\n",[146,3388,3389,3392],{"class":148,"line":184},[146,3390,3391],{"class":1214},"  rtmptarget",[146,3393,3386],{"class":152},[146,3395,3396,3399,3401],{"class":148,"line":199},[146,3397,3398],{"class":1214},"    image",[146,3400,169],{"class":152},[146,3402,3403],{"class":175}," tiangolo\u002Fnginx-rtmp\n",[146,3405,3406,3409],{"class":148,"line":205},[146,3407,3408],{"class":1214},"    ports",[146,3410,3386],{"class":152},[146,3412,3413,3416,3418,3421],{"class":148,"line":228},[146,3414,3415],{"class":152},"      -",[146,3417,172],{"class":152},[146,3419,3420],{"class":175},"1930:1935",[146,3422,293],{"class":152},[146,3424,3425,3427,3429,3432],{"class":148,"line":249},[146,3426,3415],{"class":152},[146,3428,172],{"class":152},[146,3430,3431],{"class":175},"1931:80",[146,3433,293],{"class":152},[146,3435,3436,3439],{"class":148,"line":270},[146,3437,3438],{"class":1214},"    volumes",[146,3440,3386],{"class":152},[146,3442,3443,3445],{"class":148,"line":284},[146,3444,3415],{"class":152},[146,3446,3447],{"class":175}," .\u002Fconf\u002Fnginx.conf:\u002Fetc\u002Fnginx\u002Fnginx.conf\n",[146,3449,3450,3452],{"class":148,"line":296},[146,3451,3415],{"class":152},[146,3453,3454],{"class":175}," .\u002Fconf\u002Fdefault.conf:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf\n",[146,3456,3457,3459],{"class":148,"line":302},[146,3458,3415],{"class":152},[146,3460,3461],{"class":175}," .\u002Fdebug:\u002Fusr\u002Fshare\u002Fnginx\u002F\n",[146,3463,3464,3467],{"class":148,"line":316},[146,3465,3466],{"class":1214},"  rtmp",[146,3468,3386],{"class":152},[146,3470,3471,3473,3475],{"class":148,"line":326},[146,3472,3398],{"class":1214},[146,3474,169],{"class":152},[146,3476,3403],{"class":175},[146,3478,3479,3481],{"class":148,"line":332},[146,3480,3408],{"class":1214},[146,3482,3386],{"class":152},[146,3484,3485,3487,3489,3492],{"class":148,"line":338},[146,3486,3415],{"class":152},[146,3488,172],{"class":152},[146,3490,3491],{"class":175},"1935:1935",[146,3493,293],{"class":152},[146,3495,3496,3498],{"class":148,"line":344},[146,3497,3438],{"class":1214},[146,3499,3386],{"class":152},[146,3501,3502,3504],{"class":148,"line":1396},[146,3503,3415],{"class":152},[146,3505,3506],{"class":175}," shared_data:\u002Fusr\u002Fshare\u002Fnginx\u002Fhls\n",[146,3508,3509,3511],{"class":148,"line":1420},[146,3510,3415],{"class":152},[146,3512,3447],{"class":175},[146,3514,3515,3518],{"class":148,"line":1425},[146,3516,3517],{"class":1214},"  php",[146,3519,3386],{"class":152},[146,3521,3522,3525,3527],{"class":148,"line":1431},[146,3523,3524],{"class":1214},"    build",[146,3526,169],{"class":152},[146,3528,3529],{"class":1161}," .\n",[146,3531,3532,3535],{"class":148,"line":1488},[146,3533,3534],{"class":1214},"    expose",[146,3536,3386],{"class":152},[146,3538,3539,3541],{"class":148,"line":1518},[146,3540,3415],{"class":152},[146,3542,3543],{"class":1161}," 1935\n",[146,3545,3546,3548],{"class":148,"line":1527},[146,3547,3408],{"class":1214},[146,3549,3386],{"class":152},[146,3551,3552,3554,3556,3559],{"class":148,"line":1533},[146,3553,3415],{"class":152},[146,3555,172],{"class":152},[146,3557,3558],{"class":175},"8080:80",[146,3560,293],{"class":152},[146,3562,3563,3565],{"class":148,"line":1566},[146,3564,3438],{"class":1214},[146,3566,3386],{"class":152},[146,3568,3569,3571],{"class":148,"line":1592},[146,3570,3415],{"class":152},[146,3572,3573],{"class":175}," shared_data:\u002Fvar\u002Fwww\u002Fhtml\u002Fhls\n",[146,3575,3576,3578],{"class":148,"line":1620},[146,3577,3415],{"class":152},[146,3579,3580],{"class":175}," .\u002Fsrc:\u002Fvar\u002Fwww\u002Fhtml\n",[146,3582,3583,3585],{"class":148,"line":1630},[146,3584,3415],{"class":152},[146,3586,3587],{"class":175}," .\u002Fconf\u002F.htpasswd:\u002Fvar\u002Fwww\u002F.htpasswd\n",[146,3589,3590,3592],{"class":148,"line":1636},[146,3591,3415],{"class":152},[146,3593,3594],{"class":175}," .\u002Fconf\u002F000-default.conf:\u002Fetc\u002Fapache2\u002Fsites-enabled\u002F000-default.conf\n",[146,3596,3597,3600],{"class":148,"line":1641},[146,3598,3599],{"class":1214},"    extra_hosts",[146,3601,3386],{"class":152},[146,3603,3604,3606,3608,3611],{"class":148,"line":1646},[146,3605,3415],{"class":152},[146,3607,172],{"class":152},[146,3609,3610],{"class":175},"host.docker.internal:host-gateway",[146,3612,293],{"class":152},[146,3614,3615,3618],{"class":148,"line":1658},[146,3616,3617],{"class":1214},"volumes",[146,3619,3386],{"class":152},[146,3621,3622,3625],{"class":148,"line":1675},[146,3623,3624],{"class":1214},"  shared_data",[146,3626,3386],{"class":152},[13,3628,3629],{},"主にRTMPサーバ用の内容です。",[137,3631,3636],{"className":3632,"code":3633,"filename":3634,"language":3635,"meta":142,"style":142},"language-conf shiki shiki-themes material-theme-ocean","worker_processes  auto;\n\nerror_log  \u002Fvar\u002Flog\u002Fnginx\u002Ferror.log notice;\npid        \u002Fvar\u002Frun\u002Fnginx.pid;\n\n\nevents {\n    worker_connections  1024;\n}\n\nrtmp_auto_push on;\n\nrtmp {\n    server {\n        listen 1935;\n        listen [::]:1935 ipv6only=on;\n        access_log \u002Fvar\u002Flog\u002Frtmp_access.log;\n        chunk_size 4096;\n        timeout 10s;\n\n        application live {\n            live on;\n\n            # HLSの記述欄\n            hls on;\n            # ここに映像ファイルが配置される\n            hls_path \u002Fusr\u002Fshare\u002Fnginx\u002Fhls;\n            hls_fragment 10s;\n            hls_playlist_length 30s;\n        }\n    }\n}\n\n\n\nhttp {\n    include       \u002Fetc\u002Fnginx\u002Fmime.types;\n    default_type  application\u002Foctet-stream;\n\n    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log  \u002Fvar\u002Flog\u002Fnginx\u002Faccess.log  main;\n\n    sendfile        on;\n    #tcp_nopush     on;\n\n    keepalive_timeout  65;\n\n    #gzip  on;\n\n    include \u002Fetc\u002Fnginx\u002Fconf.d\u002F*.conf;\n}\n","conf\u002Fnginx.conf","conf",[102,3637,3638,3643,3647,3652,3657,3661,3665,3670,3675,3679,3683,3688,3692,3697,3702,3707,3712,3717,3722,3727,3731,3736,3741,3745,3750,3755,3760,3765,3770,3775,3779,3783,3787,3791,3795,3799,3804,3809,3814,3818,3823,3828,3833,3837,3842,3846,3851,3856,3860,3865,3869,3874,3878,3883],{"__ignoreMap":142},[146,3639,3640],{"class":148,"line":149},[146,3641,3642],{},"worker_processes  auto;\n",[146,3644,3645],{"class":148,"line":156},[146,3646,1082],{"emptyLinePlaceholder":936},[146,3648,3649],{"class":148,"line":184},[146,3650,3651],{},"error_log  \u002Fvar\u002Flog\u002Fnginx\u002Ferror.log notice;\n",[146,3653,3654],{"class":148,"line":199},[146,3655,3656],{},"pid        \u002Fvar\u002Frun\u002Fnginx.pid;\n",[146,3658,3659],{"class":148,"line":205},[146,3660,1082],{"emptyLinePlaceholder":936},[146,3662,3663],{"class":148,"line":228},[146,3664,1082],{"emptyLinePlaceholder":936},[146,3666,3667],{"class":148,"line":249},[146,3668,3669],{},"events {\n",[146,3671,3672],{"class":148,"line":270},[146,3673,3674],{},"    worker_connections  1024;\n",[146,3676,3677],{"class":148,"line":284},[146,3678,347],{},[146,3680,3681],{"class":148,"line":296},[146,3682,1082],{"emptyLinePlaceholder":936},[146,3684,3685],{"class":148,"line":302},[146,3686,3687],{},"rtmp_auto_push on;\n",[146,3689,3690],{"class":148,"line":316},[146,3691,1082],{"emptyLinePlaceholder":936},[146,3693,3694],{"class":148,"line":326},[146,3695,3696],{},"rtmp {\n",[146,3698,3699],{"class":148,"line":332},[146,3700,3701],{},"    server {\n",[146,3703,3704],{"class":148,"line":338},[146,3705,3706],{},"        listen 1935;\n",[146,3708,3709],{"class":148,"line":344},[146,3710,3711],{},"        listen [::]:1935 ipv6only=on;\n",[146,3713,3714],{"class":148,"line":1396},[146,3715,3716],{},"        access_log \u002Fvar\u002Flog\u002Frtmp_access.log;\n",[146,3718,3719],{"class":148,"line":1420},[146,3720,3721],{},"        chunk_size 4096;\n",[146,3723,3724],{"class":148,"line":1425},[146,3725,3726],{},"        timeout 10s;\n",[146,3728,3729],{"class":148,"line":1431},[146,3730,1082],{"emptyLinePlaceholder":936},[146,3732,3733],{"class":148,"line":1488},[146,3734,3735],{},"        application live {\n",[146,3737,3738],{"class":148,"line":1518},[146,3739,3740],{},"            live on;\n",[146,3742,3743],{"class":148,"line":1527},[146,3744,1082],{"emptyLinePlaceholder":936},[146,3746,3747],{"class":148,"line":1533},[146,3748,3749],{},"            # HLSの記述欄\n",[146,3751,3752],{"class":148,"line":1566},[146,3753,3754],{},"            hls on;\n",[146,3756,3757],{"class":148,"line":1592},[146,3758,3759],{},"            # ここに映像ファイルが配置される\n",[146,3761,3762],{"class":148,"line":1620},[146,3763,3764],{},"            hls_path \u002Fusr\u002Fshare\u002Fnginx\u002Fhls;\n",[146,3766,3767],{"class":148,"line":1630},[146,3768,3769],{},"            hls_fragment 10s;\n",[146,3771,3772],{"class":148,"line":1636},[146,3773,3774],{},"            hls_playlist_length 30s;\n",[146,3776,3777],{"class":148,"line":1641},[146,3778,335],{},[146,3780,3781],{"class":148,"line":1646},[146,3782,1633],{},[146,3784,3785],{"class":148,"line":1658},[146,3786,347],{},[146,3788,3789],{"class":148,"line":1675},[146,3790,1082],{"emptyLinePlaceholder":936},[146,3792,3793],{"class":148,"line":1686},[146,3794,1082],{"emptyLinePlaceholder":936},[146,3796,3797],{"class":148,"line":1696},[146,3798,1082],{"emptyLinePlaceholder":936},[146,3800,3801],{"class":148,"line":1713},[146,3802,3803],{},"http {\n",[146,3805,3806],{"class":148,"line":1724},[146,3807,3808],{},"    include       \u002Fetc\u002Fnginx\u002Fmime.types;\n",[146,3810,3811],{"class":148,"line":1730},[146,3812,3813],{},"    default_type  application\u002Foctet-stream;\n",[146,3815,3816],{"class":148,"line":1742},[146,3817,1082],{"emptyLinePlaceholder":936},[146,3819,3820],{"class":148,"line":1747},[146,3821,3822],{},"    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n",[146,3824,3825],{"class":148,"line":2041},[146,3826,3827],{},"                      '$status $body_bytes_sent \"$http_referer\" '\n",[146,3829,3830],{"class":148,"line":2047},[146,3831,3832],{},"                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n",[146,3834,3835],{"class":148,"line":4},[146,3836,1082],{"emptyLinePlaceholder":936},[146,3838,3839],{"class":148,"line":2058},[146,3840,3841],{},"    access_log  \u002Fvar\u002Flog\u002Fnginx\u002Faccess.log  main;\n",[146,3843,3844],{"class":148,"line":2063},[146,3845,1082],{"emptyLinePlaceholder":936},[146,3847,3848],{"class":148,"line":2081},[146,3849,3850],{},"    sendfile        on;\n",[146,3852,3853],{"class":148,"line":2098},[146,3854,3855],{},"    #tcp_nopush     on;\n",[146,3857,3858],{"class":148,"line":2120},[146,3859,1082],{"emptyLinePlaceholder":936},[146,3861,3862],{"class":148,"line":2133},[146,3863,3864],{},"    keepalive_timeout  65;\n",[146,3866,3867],{"class":148,"line":2140},[146,3868,1082],{"emptyLinePlaceholder":936},[146,3870,3871],{"class":148,"line":2145},[146,3872,3873],{},"    #gzip  on;\n",[146,3875,3876],{"class":148,"line":2157},[146,3877,1082],{"emptyLinePlaceholder":936},[146,3879,3880],{"class":148,"line":2163},[146,3881,3882],{},"    include \u002Fetc\u002Fnginx\u002Fconf.d\u002F*.conf;\n",[146,3884,3885],{"class":148,"line":2169},[146,3886,347],{},[13,3888,3629],{},[137,3890,3893],{"className":3632,"code":3891,"filename":3892,"language":3635,"meta":142,"style":142},"server {\n    listen       80;\n    listen  [::]:80;\n    server_name  localhost;\n\n    #access_log  \u002Fvar\u002Flog\u002Fnginx\u002Fhost.access.log  main;\n\n    location \u002F {\n        root   \u002Fusr\u002Fshare\u002Fnginx;\n        index  index.html index.htm;\n    }\n\n    #error_page  404              \u002F404.html;\n\n    # redirect server error pages to the static page \u002F50x.html\n    #\n    error_page   500 502 503 504  \u002F50x.html;\n    location = \u002F50x.html {\n        root   \u002Fusr\u002Fshare\u002Fnginx;\n    }\n\n    # proxy the PHP scripts to Apache listening on 127.0.0.1:80\n    #\n    #location ~ \\.php$ {\n    #    proxy_pass   http:\u002F\u002F127.0.0.1;\n    #}\n\n    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n    #\n    #location ~ \\.php$ {\n    #    root           html;\n    #    fastcgi_pass   127.0.0.1:9000;\n    #    fastcgi_index  index.php;\n    #    fastcgi_param  SCRIPT_FILENAME  \u002Fscripts$fastcgi_script_name;\n    #    include        fastcgi_params;\n    #}\n\n    # deny access to .htaccess files, if Apache's document root\n    # concurs with nginx's one\n    #\n    #location ~ \u002F\\.ht {\n    #    deny  all;\n    #}\n}\n","conf\u002Fdefault.conf",[102,3894,3895,3900,3905,3910,3915,3919,3924,3928,3933,3938,3943,3947,3951,3956,3960,3965,3970,3975,3980,3984,3988,3992,3997,4001,4006,4011,4016,4020,4025,4029,4033,4038,4043,4048,4053,4058,4062,4066,4071,4076,4080,4085,4090,4094],{"__ignoreMap":142},[146,3896,3897],{"class":148,"line":149},[146,3898,3899],{},"server {\n",[146,3901,3902],{"class":148,"line":156},[146,3903,3904],{},"    listen       80;\n",[146,3906,3907],{"class":148,"line":184},[146,3908,3909],{},"    listen  [::]:80;\n",[146,3911,3912],{"class":148,"line":199},[146,3913,3914],{},"    server_name  localhost;\n",[146,3916,3917],{"class":148,"line":205},[146,3918,1082],{"emptyLinePlaceholder":936},[146,3920,3921],{"class":148,"line":228},[146,3922,3923],{},"    #access_log  \u002Fvar\u002Flog\u002Fnginx\u002Fhost.access.log  main;\n",[146,3925,3926],{"class":148,"line":249},[146,3927,1082],{"emptyLinePlaceholder":936},[146,3929,3930],{"class":148,"line":270},[146,3931,3932],{},"    location \u002F {\n",[146,3934,3935],{"class":148,"line":284},[146,3936,3937],{},"        root   \u002Fusr\u002Fshare\u002Fnginx;\n",[146,3939,3940],{"class":148,"line":296},[146,3941,3942],{},"        index  index.html index.htm;\n",[146,3944,3945],{"class":148,"line":302},[146,3946,1633],{},[146,3948,3949],{"class":148,"line":316},[146,3950,1082],{"emptyLinePlaceholder":936},[146,3952,3953],{"class":148,"line":326},[146,3954,3955],{},"    #error_page  404              \u002F404.html;\n",[146,3957,3958],{"class":148,"line":332},[146,3959,1082],{"emptyLinePlaceholder":936},[146,3961,3962],{"class":148,"line":338},[146,3963,3964],{},"    # redirect server error pages to the static page \u002F50x.html\n",[146,3966,3967],{"class":148,"line":344},[146,3968,3969],{},"    #\n",[146,3971,3972],{"class":148,"line":1396},[146,3973,3974],{},"    error_page   500 502 503 504  \u002F50x.html;\n",[146,3976,3977],{"class":148,"line":1420},[146,3978,3979],{},"    location = \u002F50x.html {\n",[146,3981,3982],{"class":148,"line":1425},[146,3983,3937],{},[146,3985,3986],{"class":148,"line":1431},[146,3987,1633],{},[146,3989,3990],{"class":148,"line":1488},[146,3991,1082],{"emptyLinePlaceholder":936},[146,3993,3994],{"class":148,"line":1518},[146,3995,3996],{},"    # proxy the PHP scripts to Apache listening on 127.0.0.1:80\n",[146,3998,3999],{"class":148,"line":1527},[146,4000,3969],{},[146,4002,4003],{"class":148,"line":1533},[146,4004,4005],{},"    #location ~ \\.php$ {\n",[146,4007,4008],{"class":148,"line":1566},[146,4009,4010],{},"    #    proxy_pass   http:\u002F\u002F127.0.0.1;\n",[146,4012,4013],{"class":148,"line":1592},[146,4014,4015],{},"    #}\n",[146,4017,4018],{"class":148,"line":1620},[146,4019,1082],{"emptyLinePlaceholder":936},[146,4021,4022],{"class":148,"line":1630},[146,4023,4024],{},"    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n",[146,4026,4027],{"class":148,"line":1636},[146,4028,3969],{},[146,4030,4031],{"class":148,"line":1641},[146,4032,4005],{},[146,4034,4035],{"class":148,"line":1646},[146,4036,4037],{},"    #    root           html;\n",[146,4039,4040],{"class":148,"line":1658},[146,4041,4042],{},"    #    fastcgi_pass   127.0.0.1:9000;\n",[146,4044,4045],{"class":148,"line":1675},[146,4046,4047],{},"    #    fastcgi_index  index.php;\n",[146,4049,4050],{"class":148,"line":1686},[146,4051,4052],{},"    #    fastcgi_param  SCRIPT_FILENAME  \u002Fscripts$fastcgi_script_name;\n",[146,4054,4055],{"class":148,"line":1696},[146,4056,4057],{},"    #    include        fastcgi_params;\n",[146,4059,4060],{"class":148,"line":1713},[146,4061,4015],{},[146,4063,4064],{"class":148,"line":1724},[146,4065,1082],{"emptyLinePlaceholder":936},[146,4067,4068],{"class":148,"line":1730},[146,4069,4070],{},"    # deny access to .htaccess files, if Apache's document root\n",[146,4072,4073],{"class":148,"line":1742},[146,4074,4075],{},"    # concurs with nginx's one\n",[146,4077,4078],{"class":148,"line":1747},[146,4079,3969],{},[146,4081,4082],{"class":148,"line":2041},[146,4083,4084],{},"    #location ~ \u002F\\.ht {\n",[146,4086,4087],{"class":148,"line":2047},[146,4088,4089],{},"    #    deny  all;\n",[146,4091,4092],{"class":148,"line":4},[146,4093,4015],{},[146,4095,4096],{"class":148,"line":2058},[146,4097,347],{},[13,4099,4100,4101,4104],{},"管理用コンテナに使用します。ドキュメントルートが",[102,4102,4103],{},"\u002Fvar\u002Fwww\u002Fhtml\u002Fpublic","なのは後でlumenを入れるためです。",[137,4106,4109],{"className":3632,"code":4107,"filename":4108,"language":3635,"meta":142,"style":142},"\u003CVirtualHost *:80>\n    # The ServerName directive sets the request scheme, hostname and port that\n    # the server uses to identify itself. This is used when creating\n    # redirection URLs. In the context of virtual hosts, the ServerName\n    # specifies what hostname must appear in the request's Host: header to\n    # match this virtual host. For the default virtual host (this file) this\n    # value is not decisive as it is used as a last resort host regardless.\n    # However, you must set it for any further virtual host explicitly.\n    #ServerName www.example.com\n\n    ServerAdmin webmaster@localhost\n    DocumentRoot \u002Fvar\u002Fwww\u002Fhtml\u002Fpublic\n\n    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,\n    # error, crit, alert, emerg.\n    # It is also possible to configure the loglevel for particular\n    # modules, e.g.\n    #LogLevel info ssl:warn\n\n    ErrorLog ${APACHE_LOG_DIR}\u002Ferror.log\n    CustomLog ${APACHE_LOG_DIR}\u002Faccess.log combined\n\n    # For most configuration files from conf-available\u002F, which are\n    # enabled or disabled at a global level, it is possible to\n    # include a line for only one particular virtual host. For example the\n    # following line enables the CGI configuration for this host only\n    # after it has been globally disabled with \"a2disconf\".\n    #Include conf-available\u002Fserve-cgi-bin.conf\n\u003C\u002FVirtualHost>\n","conf\u002F000-default.conf",[102,4110,4111,4116,4121,4126,4131,4136,4141,4146,4151,4156,4160,4165,4170,4174,4179,4184,4189,4194,4199,4203,4208,4213,4217,4222,4227,4232,4237,4242,4247],{"__ignoreMap":142},[146,4112,4113],{"class":148,"line":149},[146,4114,4115],{},"\u003CVirtualHost *:80>\n",[146,4117,4118],{"class":148,"line":156},[146,4119,4120],{},"    # The ServerName directive sets the request scheme, hostname and port that\n",[146,4122,4123],{"class":148,"line":184},[146,4124,4125],{},"    # the server uses to identify itself. This is used when creating\n",[146,4127,4128],{"class":148,"line":199},[146,4129,4130],{},"    # redirection URLs. In the context of virtual hosts, the ServerName\n",[146,4132,4133],{"class":148,"line":205},[146,4134,4135],{},"    # specifies what hostname must appear in the request's Host: header to\n",[146,4137,4138],{"class":148,"line":228},[146,4139,4140],{},"    # match this virtual host. For the default virtual host (this file) this\n",[146,4142,4143],{"class":148,"line":249},[146,4144,4145],{},"    # value is not decisive as it is used as a last resort host regardless.\n",[146,4147,4148],{"class":148,"line":270},[146,4149,4150],{},"    # However, you must set it for any further virtual host explicitly.\n",[146,4152,4153],{"class":148,"line":284},[146,4154,4155],{},"    #ServerName www.example.com\n",[146,4157,4158],{"class":148,"line":296},[146,4159,1082],{"emptyLinePlaceholder":936},[146,4161,4162],{"class":148,"line":302},[146,4163,4164],{},"    ServerAdmin webmaster@localhost\n",[146,4166,4167],{"class":148,"line":316},[146,4168,4169],{},"    DocumentRoot \u002Fvar\u002Fwww\u002Fhtml\u002Fpublic\n",[146,4171,4172],{"class":148,"line":326},[146,4173,1082],{"emptyLinePlaceholder":936},[146,4175,4176],{"class":148,"line":332},[146,4177,4178],{},"    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,\n",[146,4180,4181],{"class":148,"line":338},[146,4182,4183],{},"    # error, crit, alert, emerg.\n",[146,4185,4186],{"class":148,"line":344},[146,4187,4188],{},"    # It is also possible to configure the loglevel for particular\n",[146,4190,4191],{"class":148,"line":1396},[146,4192,4193],{},"    # modules, e.g.\n",[146,4195,4196],{"class":148,"line":1420},[146,4197,4198],{},"    #LogLevel info ssl:warn\n",[146,4200,4201],{"class":148,"line":1425},[146,4202,1082],{"emptyLinePlaceholder":936},[146,4204,4205],{"class":148,"line":1431},[146,4206,4207],{},"    ErrorLog ${APACHE_LOG_DIR}\u002Ferror.log\n",[146,4209,4210],{"class":148,"line":1488},[146,4211,4212],{},"    CustomLog ${APACHE_LOG_DIR}\u002Faccess.log combined\n",[146,4214,4215],{"class":148,"line":1518},[146,4216,1082],{"emptyLinePlaceholder":936},[146,4218,4219],{"class":148,"line":1527},[146,4220,4221],{},"    # For most configuration files from conf-available\u002F, which are\n",[146,4223,4224],{"class":148,"line":1533},[146,4225,4226],{},"    # enabled or disabled at a global level, it is possible to\n",[146,4228,4229],{"class":148,"line":1566},[146,4230,4231],{},"    # include a line for only one particular virtual host. For example the\n",[146,4233,4234],{"class":148,"line":1592},[146,4235,4236],{},"    # following line enables the CGI configuration for this host only\n",[146,4238,4239],{"class":148,"line":1620},[146,4240,4241],{},"    # after it has been globally disabled with \"a2disconf\".\n",[146,4243,4244],{"class":148,"line":1630},[146,4245,4246],{},"    #Include conf-available\u002Fserve-cgi-bin.conf\n",[146,4248,4249],{"class":148,"line":1636},[146,4250,4251],{},"\u003C\u002FVirtualHost>\n",[13,4253,4254],{},"とりあえず確認したい時に使用します。",[137,4256,4259],{"className":2651,"code":4257,"filename":4258,"language":2653,"meta":142,"style":142},"\u003C!DOCTYPE html>\n\u003Chtml>\n\n\u003Chead>\n  \u003Cmeta charset=\"utf-8\">\n  \u003Ctitle>MediaElement\u003C\u002Ftitle>\n  \u003C!-- MediaElement style -->\n  \u003Clink rel=\"stylesheet\" href=\"\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fmediaelement\u002F4.2.9\u002Fmediaelementplayer.css\" \u002F>\n\u003C\u002Fhead>\n\n\u003Cbody>\n  \u003C!-- MediaElement -->\n  \u003Cscript src=\"\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fmediaelement\u002F4.2.9\u002Fmediaelement-and-player.js\">\u003C\u002Fscript>\n\n  \u003Cvideo id=\"player\" width=\"640\" height=\"360\">\n\u003C\u002Fbody>\n\u003Cscript type=\"text\u002Fjavascript\">\n\n      var player = new MediaElementPlayer('player', {\n        success: function(mediaElement, originalNode) {\n          console.log(\"Player initialised\");\n        }\n      });\n        player.setSrc(\"\u002Fhls\u002F.m3u8\");\n\u003C\u002Fscript>\n\n\u003C\u002Fhtml>\n","src\u002Fpublic\u002Ftest.html,debug\u002Findex.html",[102,4260,4261,4271,4279,4283,4291,4313,4330,4335,4364,4372,4376,4384,4389,4412,4416,4460,4468,4488,4492,4520,4544,4566,4570,4579,4602,4610,4614],{"__ignoreMap":142},[146,4262,4263,4265,4267,4269],{"class":148,"line":149},[146,4264,2660],{"class":152},[146,4266,2663],{"class":1214},[146,4268,2666],{"class":162},[146,4270,2669],{"class":152},[146,4272,4273,4275,4277],{"class":148,"line":156},[146,4274,1170],{"class":152},[146,4276,2653],{"class":1214},[146,4278,2669],{"class":152},[146,4280,4281],{"class":148,"line":184},[146,4282,1082],{"emptyLinePlaceholder":936},[146,4284,4285,4287,4289],{"class":148,"line":199},[146,4286,1170],{"class":152},[146,4288,2684],{"class":1214},[146,4290,2669],{"class":152},[146,4292,4293,4296,4299,4302,4304,4306,4309,4311],{"class":148,"line":205},[146,4294,4295],{"class":152},"  \u003C",[146,4297,4298],{"class":1214},"meta",[146,4300,4301],{"class":162}," charset",[146,4303,1093],{"class":152},[146,4305,166],{"class":152},[146,4307,4308],{"class":175},"utf-8",[146,4310,166],{"class":152},[146,4312,2669],{"class":152},[146,4314,4315,4317,4319,4321,4324,4326,4328],{"class":148,"line":228},[146,4316,4295],{"class":152},[146,4318,2694],{"class":1214},[146,4320,2697],{"class":152},[146,4322,4323],{"class":866},"MediaElement",[146,4325,2703],{"class":152},[146,4327,2694],{"class":1214},[146,4329,2669],{"class":152},[146,4331,4332],{"class":148,"line":249},[146,4333,4334],{"class":1141},"  \u003C!-- MediaElement style -->\n",[146,4336,4337,4339,4341,4343,4345,4347,4349,4351,4353,4355,4357,4360,4362],{"class":148,"line":270},[146,4338,4295],{"class":152},[146,4340,2714],{"class":1214},[146,4342,2717],{"class":162},[146,4344,1093],{"class":152},[146,4346,166],{"class":152},[146,4348,2724],{"class":175},[146,4350,166],{"class":152},[146,4352,2729],{"class":162},[146,4354,1093],{"class":152},[146,4356,166],{"class":152},[146,4358,4359],{"class":175},"\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fmediaelement\u002F4.2.9\u002Fmediaelementplayer.css",[146,4361,166],{"class":152},[146,4363,2741],{"class":152},[146,4365,4366,4368,4370],{"class":148,"line":284},[146,4367,2703],{"class":152},[146,4369,2684],{"class":1214},[146,4371,2669],{"class":152},[146,4373,4374],{"class":148,"line":296},[146,4375,1082],{"emptyLinePlaceholder":936},[146,4377,4378,4380,4382],{"class":148,"line":302},[146,4379,1170],{"class":152},[146,4381,2782],{"class":1214},[146,4383,2669],{"class":152},[146,4385,4386],{"class":148,"line":316},[146,4387,4388],{"class":1141},"  \u003C!-- MediaElement -->\n",[146,4390,4391,4393,4395,4397,4399,4401,4404,4406,4408,4410],{"class":148,"line":326},[146,4392,4295],{"class":152},[146,4394,2748],{"class":1214},[146,4396,2751],{"class":162},[146,4398,1093],{"class":152},[146,4400,166],{"class":152},[146,4402,4403],{"class":175},"\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fmediaelement\u002F4.2.9\u002Fmediaelement-and-player.js",[146,4405,166],{"class":152},[146,4407,2763],{"class":152},[146,4409,2748],{"class":1214},[146,4411,2669],{"class":152},[146,4413,4414],{"class":148,"line":332},[146,4415,1082],{"emptyLinePlaceholder":936},[146,4417,4418,4420,4423,4425,4427,4429,4432,4434,4437,4439,4441,4444,4446,4449,4451,4453,4456,4458],{"class":148,"line":338},[146,4419,4295],{"class":152},[146,4421,4422],{"class":1214},"video",[146,4424,2793],{"class":162},[146,4426,1093],{"class":152},[146,4428,166],{"class":152},[146,4430,4431],{"class":175},"player",[146,4433,166],{"class":152},[146,4435,4436],{"class":162}," width",[146,4438,1093],{"class":152},[146,4440,166],{"class":152},[146,4442,4443],{"class":175},"640",[146,4445,166],{"class":152},[146,4447,4448],{"class":162}," height",[146,4450,1093],{"class":152},[146,4452,166],{"class":152},[146,4454,4455],{"class":175},"360",[146,4457,166],{"class":152},[146,4459,2669],{"class":152},[146,4461,4462,4464,4466],{"class":148,"line":344},[146,4463,2703],{"class":152},[146,4465,2782],{"class":1214},[146,4467,2669],{"class":152},[146,4469,4470,4472,4474,4477,4479,4481,4484,4486],{"class":148,"line":1396},[146,4471,1170],{"class":152},[146,4473,2748],{"class":1214},[146,4475,4476],{"class":162}," type",[146,4478,1093],{"class":152},[146,4480,166],{"class":152},[146,4482,4483],{"class":175},"text\u002Fjavascript",[146,4485,166],{"class":152},[146,4487,2669],{"class":152},[146,4489,4490],{"class":148,"line":1420},[146,4491,1082],{"emptyLinePlaceholder":936},[146,4493,4494,4497,4500,4502,4505,4508,4510,4512,4514,4516,4518],{"class":148,"line":1425},[146,4495,4496],{"class":162},"      var",[146,4498,4499],{"class":866}," player ",[146,4501,1093],{"class":152},[146,4503,4504],{"class":152}," new",[146,4506,4507],{"class":1268}," MediaElementPlayer",[146,4509,1360],{"class":866},[146,4511,1049],{"class":152},[146,4513,4431],{"class":175},[146,4515,1049],{"class":152},[146,4517,1099],{"class":152},[146,4519,861],{"class":152},[146,4521,4522,4525,4527,4530,4532,4535,4537,4540,4542],{"class":148,"line":1431},[146,4523,4524],{"class":1268},"        success",[146,4526,169],{"class":152},[146,4528,4529],{"class":162}," function",[146,4531,1360],{"class":152},[146,4533,4534],{"class":1581},"mediaElement",[146,4536,1099],{"class":152},[146,4538,4539],{"class":1581}," originalNode",[146,4541,1384],{"class":152},[146,4543,861],{"class":152},[146,4545,4546,4549,4551,4553,4555,4557,4560,4562,4564],{"class":148,"line":1488},[146,4547,4548],{"class":866},"          console",[146,4550,1176],{"class":152},[146,4552,1880],{"class":1268},[146,4554,1360],{"class":1214},[146,4556,166],{"class":152},[146,4558,4559],{"class":175},"Player initialised",[146,4561,166],{"class":152},[146,4563,1384],{"class":1214},[146,4565,1052],{"class":152},[146,4567,4568],{"class":148,"line":1518},[146,4569,335],{"class":152},[146,4571,4572,4575,4577],{"class":148,"line":1527},[146,4573,4574],{"class":152},"      }",[146,4576,1384],{"class":866},[146,4578,1052],{"class":152},[146,4580,4581,4584,4586,4589,4591,4593,4596,4598,4600],{"class":148,"line":1533},[146,4582,4583],{"class":866},"        player",[146,4585,1176],{"class":152},[146,4587,4588],{"class":1268},"setSrc",[146,4590,1360],{"class":866},[146,4592,166],{"class":152},[146,4594,4595],{"class":175},"\u002Fhls\u002F.m3u8",[146,4597,166],{"class":152},[146,4599,1384],{"class":866},[146,4601,1052],{"class":152},[146,4603,4604,4606,4608],{"class":148,"line":1566},[146,4605,2703],{"class":152},[146,4607,2748],{"class":1214},[146,4609,2669],{"class":152},[146,4611,4612],{"class":148,"line":1592},[146,4613,1082],{"emptyLinePlaceholder":936},[146,4615,4616,4618,4620],{"class":148,"line":1620},[146,4617,2703],{"class":152},[146,4619,2653],{"class":1214},[146,4621,2669],{"class":152},[13,4623,4624],{},"では上記のファイルを用いて解説します。",[46,4626,4627],{"id":4627},"共有ボリュームを用いて受信した映像を扱う",[13,4629,4630],{},"まずは受信用コンテナから得られたGoproの映像を管理用コンテナで使用できるようにします。受信用コンテナにPHPなどを入れようとしましたが結構面倒だったので、別のコンテナとして独立させました。しかしそのままでは受信した映像データ（HLS）を共有できないため、受信用コンテナで生成されるHLSファイルを共有ボリュームで共有します。",[13,4632,4633],{},"その設定のために",[137,4635,4637],{"className":3359,"code":4636,"filename":3361,"language":3362,"meta":142,"style":142},"version: '2'\nservices:\n  #...\n  rtmp:\n    #...\n    volumes:\n      - shared_data:\u002Fusr\u002Fshare\u002Fnginx\u002Fhls\n    #...\n  php:\n    #...\n    volumes:\n      - shared_data:\u002Fvar\u002Fwww\u002Fhtml\u002Fhls\n    #...\nvolumes:\n  shared_data:\n",[102,4638,4639,4651,4657,4662,4668,4673,4679,4685,4689,4695,4699,4705,4711,4715,4721],{"__ignoreMap":142},[146,4640,4641,4643,4645,4647,4649],{"class":148,"line":149},[146,4642,3369],{"class":1214},[146,4644,169],{"class":152},[146,4646,1043],{"class":152},[146,4648,3376],{"class":175},[146,4650,2979],{"class":152},[146,4652,4653,4655],{"class":148,"line":156},[146,4654,3383],{"class":1214},[146,4656,3386],{"class":152},[146,4658,4659],{"class":148,"line":184},[146,4660,4661],{"class":1141},"  #...\n",[146,4663,4664,4666],{"class":148,"line":199},[146,4665,3466],{"class":1214},[146,4667,3386],{"class":152},[146,4669,4670],{"class":148,"line":205},[146,4671,4672],{"class":1141},"    #...\n",[146,4674,4675,4677],{"class":148,"line":228},[146,4676,3438],{"class":1214},[146,4678,3386],{"class":152},[146,4680,4681,4683],{"class":148,"line":249},[146,4682,3415],{"class":152},[146,4684,3506],{"class":175},[146,4686,4687],{"class":148,"line":270},[146,4688,4672],{"class":1141},[146,4690,4691,4693],{"class":148,"line":284},[146,4692,3517],{"class":1214},[146,4694,3386],{"class":152},[146,4696,4697],{"class":148,"line":296},[146,4698,4672],{"class":1141},[146,4700,4701,4703],{"class":148,"line":302},[146,4702,3438],{"class":1214},[146,4704,3386],{"class":152},[146,4706,4707,4709],{"class":148,"line":316},[146,4708,3415],{"class":152},[146,4710,3573],{"class":175},[146,4712,4713],{"class":148,"line":326},[146,4714,4672],{"class":1141},[146,4716,4717,4719],{"class":148,"line":332},[146,4718,3617],{"class":1214},[146,4720,3386],{"class":152},[146,4722,4723,4725],{"class":148,"line":338},[146,4724,3624],{"class":1214},[146,4726,3386],{"class":152},[13,4728,4729,4731,4732,4734,4735,4738,4739,4742,4743,4746,4747,4749,4750,4753],{},[102,4730,3617],{},"でボリュームを定義してそのボリューム名を各コンテナに配置するパスを指定します。すると",[102,4733,3349],{},"（受信用コンテナ）では",[102,4736,4737],{},"\u002Fusr\u002Fshare\u002Fnginx\u002Fhls"," に",[102,4740,4741],{},"*.ts","ファイルや",[102,4744,4745],{},".m3u8","ファイルが作成されます。そして",[102,4748,3355],{},"（管理用コンテナ）では",[102,4751,4752],{},"\u002Fvar\u002Fwww\u002Fhtml\u002Fhls","に同じファイル群が作成されます。",[13,4755,4756],{},"管理用コンテナでgoproから受信したデータをffmpegで色々したり、読み取ったり、配信できるようになりました。",[419,4758,4759],{"id":4759},"テストしてみる",[13,4761,4762,4763,4766],{},"その１の記事または後述するOBSの方法を参考にGopro・OBSから",[102,4764,4765],{},"rtmp:\u002F\u002FYOUR_PC_IP\u002Flive"," にRTMP送信をします。",[137,4768,4771],{"className":4769,"code":4770,"language":3289},[3287],"# \u003CYOUR_PHP_CONTAINER_NAME>はあなたの環境の管理用コンテナ名に書き換えてください。\n\ndocker exec -it \u003CYOUR_PHP_CONTAINER_NAME> \u002Fbin\u002Fbash\n",[102,4772,4770],{"__ignoreMap":142},[13,4774,4775,4776,4778],{},"ボリュームのため、",[102,4777,4752],{},"というディレクトリがあると思います。そしてGoproでの収録を開始するとディレクトリ内にファイルが配置されます。",[13,4780,4781,4782,4785,4786,4789],{},"そして",[102,4783,4784],{},"\u002Fvar\u002Fwww\u002Fhtml\u002Fpublic\u002Ftest.html","があるので",[102,4787,4788],{},"http:\u002F\u002Flocalhost:8080","でweb playerを用いて確認できるはずです。",[46,4791,4793],{"id":4792},"ffmpegを用いてyoutubeに送信してみる","ffmpegを用いてYoutubeに送信してみる",[13,4795,4796],{},"次にffmpegを用いて管理用コンテナからyoutube liveに流してみます。ffmpegはlinux上で動画、音声の変換・記録・再生・配信を行うことができるOSSです。今はコマンド上で配信をするだけですが、最終的にはffmpegを利用して管理画面から配信内容を制御する予定です。現時点で自分もまだ詳細なプロパティーや映像系の知識に乏しいので、調べた際のコマンドのコピペやchat gptで調査した内容になります。",[419,4798,4800],{"id":4799},"youtube-のライブを始めておく","youtube のライブを始めておく",[13,4802,4803],{},"本記事では簡単に述べておきます。すでにGoogle、Youtubeアカウントがありライブストリーミングが有効になっている前提とします。",[398,4805,4806,4809,4812],{},[20,4807,4808],{},"Youtubeに移動し、右上の「＋」ボタンをクリックして「ライブ配信を開始」を選択",[20,4810,4811],{},"ライブ配信の設定からストリームURLとストリームキーを控えます。ライブストリームキーとURLは「rtmp:\u002F\u002Fa.rtmp.youtube.com\u002Flive2\u002FLIVE_KEY」とURLの後につければOKです。ただしドメインのとこはyoutubeのrtmp配信サーバのipにしておいてください（調べ方と理由は後述）",[20,4813,4814],{},"Youtubeのライブ管理画面では「ライブ配信を公開するには、ストリーミング ソフトで動画の送信を開始します」という表示がされています。ffmpegでRTMP送信をすれば表示されます。",[13,4816,4817],{},"プライバシーを非公開にしておくことをおすすめします。",[419,4819,4820],{"id":4820},"コマンドを打つ",[13,4822,4823],{},"Goproからの映像をローカルで確認できたらとりあえず以下のコマンドを管理用コンテナ内から打ってみてください。",[137,4825,4828],{"className":4826,"code":4827,"language":3289},[3287],"ffmpeg -re -i \"\u002Fvar\u002Fwww\u002Fhtml\u002Fhls\u002F.m3u8\" -c:a aac -ar 44100 -ab 128k -ac 2 -strict -2 -flags +global_header -bsf:a aac_adtstoasc -f flv \"rtmp:\u002F\u002F64.233.189.134\u002Flive2\u002FYOUR_LIVE_KEY\"\n",[102,4829,4827],{"__ignoreMap":142},[13,4831,4832,4835],{},[102,4833,4834],{},"YOUR_LIVE_KEY"," はyoutuebのライブストリームキーを使用してください。ffmpegが起動してyoutubeにデータが送信されます。20秒ぐらいするとGoproからの映像の配信が開始されるはずです。",[419,4837,4839],{"id":4838},"dnsエラー","DNSエラー？",[13,4841,4842,4843,4846],{},"本来ライブストリームURLには",[102,4844,4845],{},"a.rtmp.youtube.com","というドメインを使用すべきですが、私のdockerで管理用コンテナから実行するとffmpegが以下のようなDNSエラーが発生します。",[137,4848,4851],{"className":4849,"code":4850,"language":3289},[3287],"[tcp @ 0x696d2c0] Failed to resolve hostname a.rtmp.youtube.com: System error\n[rtmp @ 0x696d480] Cannot open connection tcp:\u002F\u002Fa.rtmp.youtube.com:1935?tcp_nodelay=0\n[out#0\u002Fflv @ 0x6890880] Error opening output rtmp:\u002F\u002Fa.rtmp.youtube.com\u002Flive2\u002Fefth-usbu-ye2g-f0sk-a7ta: Input\u002Foutput error\nError opening output file rtmp:\u002F\u002Fa.rtmp.youtube.com\u002Flive2\u002Fefth-usbu-ye2g-f0sk-a7ta.\nError opening output files: Input\u002Foutput error\n",[102,4852,4850],{"__ignoreMap":142},[13,4854,4855,4856,4858,4859,4861,4862,4864],{},"ホストマシン上でffmpegを使用すると問題なく配信できるますが、docker内から行うとなぜか",[102,4857,4845],{},"のホストが見つからないというエラーが発生します。そのためホストマシンで",[102,4860,637],{},"を使用して",[102,4863,4845],{},"のIPを調べて、直接IPを指定したら問題なく動作しました。",[13,4866,4867],{},"ここはdockerの問題かつまだ調査中です。Googleなどはこの辺のIPをころころ変えてきそうなので、最終的にはきちんとドメインを利用できるようにしますがとりあえず今はIPで指定します。",[46,4869,4870],{"id":4870},"ローカルでデバッグしやすい環境にする",[13,4872,4873],{},"ひととおり受信→中継→配信を行えるdockerコンテナが構築できました。しかし毎回goproを起動したり、youtube liveを開始するのは面倒ですし、複数人で開発する時に機材がなかったり送信先が重複してします。そのため開発用にデバッグできるように送信元、配信先をローカルで実現できるようにします。",[419,4875,4877],{"id":4876},"送信元にgoproの代わりにobsを利用する","送信元にgoproの代わりにOBSを利用する",[13,4879,4880],{},"この記事では送信元としてGoproを使用してホストマシンの受信用コンテナにRTMPを送信しています。ですがこれはOBSなどの配信ソフトを利用することで簡単に代替できます。",[13,4882,4883,4884],{},"OBSはフリーのライブ配信や録画が行えます。PC上のウィンドウキャプチャをしたり、機材をそろえればnintedo switchなどのゲーム機の映像を配信することができます。とりあえずOBSをインストールして、webカメラなりウィンドウキャプチャを映像・音声ソースとして利用します。",[53,4885,4888],{"href":4886,"rel":4887},"https:\u002F\u002Fobsproject.com\u002Fja\u002Fdownload",[57],"OBSのインストールはこちらから",[819,4890,4892],{"id":4891},"とりあえずmacの映像と音声を受信用rtmpに流そう","とりあえずmacの映像と音声を受信用RTMPに流そう",[13,4894,4895],{},"OBSをインストールしたらアプリを開きます。以下のような似た画面になっているはずです。（すでに設定がいくつかあるのは気にしないでください）",[74,4897],{":src":4898,":width":77},"'rtmp-docker-local\u002Fobs1.png'",[13,4900,4901],{},"「シーン」に何もなければ「＋」をクリックしてシーンを追加します。そして映像・音声ソースも追加していきます。「ソース」のとこの「＋」をクリックして「映像キャプチャデバイス」を選択します。",[13,4903,4904],{},"ソースのラベルを適当に入れておきます。",[74,4906],{":src":4907,":width":4908,":center":4909},"'rtmp-docker-local\u002Fobs2.png'","'400px'","true",[13,4911,4912],{},"ソースの選択画面に移動するので「デバイス」から「FaceTime HDカメラ」などを選択しておきます。",[74,4914],{":src":4915,":width":77},"'rtmp-docker-local\u002Fobs3.png'",[13,4917,4918],{},"これだとまだ映像しか拾わないので次は「ソース」のとこの「＋」をクリックして「音声入力キャプチャ」を選択します。同じようにソースのラベルを適当に入れておきます。ソースの選択画面に移動するので「デバイス」から「既定」などを選択しておきます。どれでもとりあえず音声が取得できればOKです。",[74,4920],{":src":4921,":width":77},"'rtmp-docker-local\u002Fobs4.png'",[13,4923,4924],{},"こんな感じで映像となんか奇声を発して「音声入力キャプチャ」が反応していればOKです。",[74,4926],{":src":4927,":width":77},"'rtmp-docker-local\u002Fobs5.png'",[13,4929,4930],{},"次に配信の設定をします。OBS→「設定」を開いて配信のタブをクリックします。\nサービスから「カスタム」を選択します。",[74,4932],{":src":4933,":width":77},"'rtmp-docker-local\u002Fobs6.png'",[13,4935,4936,4937,4940,4941,4944],{},"そして「サーバー」に受信用コンテナのRTMPパスをいれます。このとき",[102,4938,4939],{},"127.0.0.1","というローカルループバックアドレスにしておきます。localhostにするとうまくいかない時があります。自分は",[102,4942,4943],{},"localhost:1935","に受信用コンテナのエンドポイントが控えていますが、違うポートの時はそのポートを指定してください。「適用」をクリックして「OK」をクリックします。",[74,4946],{":src":4947,":width":77},"'rtmp-docker-local\u002Fobs7.png'",[13,4949,4950],{},"最初の画面にもどったら「配信開始」をクリックすると配信されます。下の方にこのような表示がされていたり、確認用playerで確認したり、HLSファイルが生成されていれば成功しています。",[74,4952],{":src":4953,":width":77},"'rtmp-docker-local\u002Fobs8.png'",[13,4955,4956],{},"終了する時は「配信終了」をクリックします。",[46,4958,4960],{"id":4959},"配信先にローカルの別のrtmpサーバを使用する","配信先にローカルの別のRTMPサーバを使用する。",[13,4962,4963],{},"最後に配信先のRTMPサーバをローカルに用意します。一番簡単な方法としてはもう1つRTMPサーバを立ててしまうことです。気をつける点としては管理用コンテナから配信先のホストを指定できるようにします。",[137,4965,4967],{"className":3359,"code":4966,"filename":3361,"language":3362,"meta":142,"style":142},"version: '2'\nservices:\n  rtmptarget:\n    image: tiangolo\u002Fnginx-rtmp\n    ports:\n      - \"1930:1935\"\n      - \"1931:80\"\n    volumes:\n      - .\u002Fconf\u002Fnginx.conf:\u002Fetc\u002Fnginx\u002Fnginx.conf\n      - .\u002Fconf\u002Fdefault.conf:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf\n      - .\u002Fdebug:\u002Fusr\u002Fshare\u002Fnginx\u002F\n    # ...\n  php:\n    # ...\n    extra_hosts:\n      - \"host.docker.internal:host-gateway\"\n    # ...\nvolumes:\n  shared_data:\n",[102,4968,4969,4981,4987,4993,5001,5007,5017,5027,5033,5039,5045,5051,5056,5062,5066,5072,5082,5086,5092],{"__ignoreMap":142},[146,4970,4971,4973,4975,4977,4979],{"class":148,"line":149},[146,4972,3369],{"class":1214},[146,4974,169],{"class":152},[146,4976,1043],{"class":152},[146,4978,3376],{"class":175},[146,4980,2979],{"class":152},[146,4982,4983,4985],{"class":148,"line":156},[146,4984,3383],{"class":1214},[146,4986,3386],{"class":152},[146,4988,4989,4991],{"class":148,"line":184},[146,4990,3391],{"class":1214},[146,4992,3386],{"class":152},[146,4994,4995,4997,4999],{"class":148,"line":199},[146,4996,3398],{"class":1214},[146,4998,169],{"class":152},[146,5000,3403],{"class":175},[146,5002,5003,5005],{"class":148,"line":205},[146,5004,3408],{"class":1214},[146,5006,3386],{"class":152},[146,5008,5009,5011,5013,5015],{"class":148,"line":228},[146,5010,3415],{"class":152},[146,5012,172],{"class":152},[146,5014,3420],{"class":175},[146,5016,293],{"class":152},[146,5018,5019,5021,5023,5025],{"class":148,"line":249},[146,5020,3415],{"class":152},[146,5022,172],{"class":152},[146,5024,3431],{"class":175},[146,5026,293],{"class":152},[146,5028,5029,5031],{"class":148,"line":270},[146,5030,3438],{"class":1214},[146,5032,3386],{"class":152},[146,5034,5035,5037],{"class":148,"line":284},[146,5036,3415],{"class":152},[146,5038,3447],{"class":175},[146,5040,5041,5043],{"class":148,"line":296},[146,5042,3415],{"class":152},[146,5044,3454],{"class":175},[146,5046,5047,5049],{"class":148,"line":302},[146,5048,3415],{"class":152},[146,5050,3461],{"class":175},[146,5052,5053],{"class":148,"line":316},[146,5054,5055],{"class":1141},"    # ...\n",[146,5057,5058,5060],{"class":148,"line":326},[146,5059,3517],{"class":1214},[146,5061,3386],{"class":152},[146,5063,5064],{"class":148,"line":332},[146,5065,5055],{"class":1141},[146,5067,5068,5070],{"class":148,"line":338},[146,5069,3599],{"class":1214},[146,5071,3386],{"class":152},[146,5073,5074,5076,5078,5080],{"class":148,"line":344},[146,5075,3415],{"class":152},[146,5077,172],{"class":152},[146,5079,3610],{"class":175},[146,5081,293],{"class":152},[146,5083,5084],{"class":148,"line":1396},[146,5085,5055],{"class":1141},[146,5087,5088,5090],{"class":148,"line":1420},[146,5089,3617],{"class":1214},[146,5091,3386],{"class":152},[146,5093,5094,5096],{"class":148,"line":1425},[146,5095,3624],{"class":1214},[146,5097,3386],{"class":152},[13,5099,5100,5101,5104,5105,5107],{},"管理用コンテナのプロパティに",[102,5102,5103],{},"extra_hosts","に",[102,5106,3610],{},"を加えることで、コンテナから利用できるホストマシンの特殊なIPアドレスがコンテナのhostsに追記されます。",[13,5109,5110,5113],{},[102,5111,5112],{},"cat \u002Fetc\u002Fhosts","を管理用サーバで実行すると追加したhostのIPがあるはずです。",[137,5115,5118],{"className":5116,"code":5117,"language":3289},[3287],"127.0.0.1       localhost\n::1     localhost ip6-localhost ip6-loopback\nfe00::0 ip6-localnet\nff00::0 ip6-mcastprefix\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters\n192.168.65.2    host.docker.internal ←これ\n172.23.0.3      8227ce3421ec\n",[102,5119,5117],{"__ignoreMap":142},[419,5121,5122],{"id":5122},"ffmpegを実行する",[13,5124,5125],{},"次に配信先のRTMPコンテナに向けてffmpegを用いて配信します。ローカルのホスト、ポート、パスに合わせればいいだけですが以下のコマンドを使用します。（youtubeの時とすこし違います！！）",[137,5127,5130],{"className":5128,"code":5129,"language":3289},[3287],"ffmpeg -re -i \"\u002Fvar\u002Fwww\u002Fhtml\u002Fhls\u002F.m3u8\" -c:v libx264 -preset ultrafast -c:a aac -ar 44100 -ab 128k -ac 2 -f flv \"rtmp:\u002F\u002F192.168.65.2:1930\u002Flive\"\n",[102,5131,5129],{"__ignoreMap":142},[13,5133,5134,5135,5138],{},"このコマンドを打って配信先コンテナの確認用htmlのプレイヤー",[102,5136,5137],{},"http:\u002F\u002Flocalhost:1931\u002Findex.html","から映像を確認できれば受信できています。（20秒ほど待つといいです）",[13,5140,5141],{},"すこしオプションの内容がyoutubeのときと違っている理由として、単純にyoutubeのIPから変えればいいと思っていたらなぜかweb playerで映像が見れず音声しか流れなかったのが原因です。色々探ってこのコマンドにしたら問題なくいけました。もしかするとIPだけ変更してyoutubeもこのコマンドでやったらほうがいいかもしれません。検証中です。",[46,5143,5144],{"id":5144},"まとめと参考文献",[13,5146,5147],{},"記事は以上となります。一通り管理用の中継コンテナを用いてgoproの映像をffmpegでyoutubeに送信したり、ローカルの開発環境を整えました。次回はffmpegを用いて任意のシーンに切り替えたり、goproからの映像が途絶えた時などの例外処理を加えていくwebシステム部分をphpを使用して実装しようと思います。",[13,5149,5150],{},"今回参考にした資料です。",[17,5152,5153,5160],{},[20,5154,5155],{},[53,5156,5159],{"href":5157,"rel":5158},"https:\u002F\u002Fgist.github.com\u002Fqntmpkts\u002F403a027a4bfe99812ebf8952c41f8789",[57],"Restream m3u8 stream with ffmpeg to youtube live",[20,5161,5162],{},[53,5163,5166],{"href":5164,"rel":5165},"https:\u002F\u002Fqiita.com\u002FCyberRex\u002Fitems\u002F960bbd0f348ad8dca544",[57],"FFmpegでよく使う例、コーデックをまとめてみた（2023年版）",[908,5168,5169],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .s7ZW3, html code.shiki .s7ZW3{--shiki-default:#BABED8;--shiki-default-font-style:italic}",{"title":142,"searchDepth":184,"depth":184,"links":5171},[5172,5173,5174,5177,5182,5187,5190],{"id":3258,"depth":156,"text":3258},{"id":3280,"depth":156,"text":3280},{"id":4627,"depth":156,"text":4627,"children":5175},[5176],{"id":4759,"depth":184,"text":4759},{"id":4792,"depth":156,"text":4793,"children":5178},[5179,5180,5181],{"id":4799,"depth":184,"text":4800},{"id":4820,"depth":184,"text":4820},{"id":4838,"depth":184,"text":4839},{"id":4870,"depth":156,"text":4870,"children":5183},[5184],{"id":4876,"depth":184,"text":4877,"children":5185},[5186],{"id":4891,"depth":199,"text":4892},{"id":4959,"depth":156,"text":4960,"children":5188},[5189],{"id":5122,"depth":184,"text":5122},{"id":5144,"depth":156,"text":5144},[930],"2024-07-09","youtbeへの配信テストとデバッグ環境構築",{},"\u002Fseries\u002Frtmp-manager-server-2",{"title":3230,"description":5193},"rtmp-manager-server","Goproの配信映像を中継してyoutubeへの配信を管理するwebシステムを開発する","series\u002Frtmp-manager-server-2",[5201,5202],"docker","nginx","rtmp-docker-local\u002Fobs5.png","HPFKEu4iBy3O40z_uOHtY7Ed_jACJ4aBXKcnLMlAOLo",{"id":5206,"title":5207,"body":5208,"category":6281,"createdAt":6282,"description":6283,"extension":933,"index":934,"meta":6284,"navigation":936,"path":6285,"publish":936,"seo":6286,"series":934,"seriesTitle":934,"stem":6287,"tag":6288,"thumbnail":934,"updatedAt":934,"__hash__":6289},"articles\u002Farticles\u002Fsearch-console-index-programmatically.md","Google Search ConsoleのインデックスリクエストをAPI通じて行う方法",{"type":10,"value":5209,"toc":6273},[5210,5218,5222,5229,5232,5235,5238,5241,5244,5253,5258,5261,5264,5270,5276,5279,5282,5285,5294,5297,5304,5315,5318,5321,5324,5327,5330,5333,5336,5339,5342,5346,5349,5354,5357,5360,5363,5366,5369,5372,5376,5385,5388,5391,6259,6262,6265,6268,6270],[13,5211,5212,5213,5217],{},"実装をさっさとみたい方は",[53,5214,5216],{"href":5215},"#api%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E7%9A%84%E3%81%AB%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B","APIを使用してプログラム的にリクエストする","へ移動してください。",[46,5219,5221],{"id":5220},"ページがgoogleに乗らない状態に","ページがGoogleに乗らない状態に",[13,5223,5224,5225,5228],{},"個人開発で作成したwebサービス ",[53,5226,2419],{"href":2417,"rel":5227},[57],"のユーザーインプレッション数を伸ばすため、Crowdワークスでライターを募集して記事をたくさん作成したが一向にOrganig Searchが伸びない現象に悩んでいました。",[13,5230,5231],{},"おもむろにSearch Consoleを調べている時に「インデックスの登録」というものを見つけました。すると",[74,5233],{":src":5234,":width":77},"'search-console-index-programmatically\u002Fsearch-console-index.png'",[13,5236,5237],{},"未登録の部分が200件となっていました。この未登録とはGoogle上で配信されていないという意味になります。つまり記事を書いたのにもかかわらず、記事が検索結果にでてこないというやばい状態です。",[13,5239,5240],{},"もう少し下に移動するとその未登録の理由がわかります。",[74,5242],{":src":5243,":width":77},"'search-console-index-programmatically\u002Findex-reason.png'",[13,5245,5246,5247,5252],{},"自分の場合はこの「検出」というとこが200件ありました。この",[53,5248,5251],{"href":5249,"rel":5250},"https:\u002F\u002Fsupport.google.com\u002Fwebmasters\u002Fanswer\u002F7440203#discovered__unclear_status",[57],"検出というのは公式いわく","、",[671,5254,5255],{},[13,5256,5257],{},"ページは Google により検出されましたが、まだクロールされていません。これは通常、Google が URL をクロールしようとしたものの、サイトへの過負荷が予想されたため、クロールの再スケジュールが必要となった場合です。そのため、レポート上で最終クロール日が空欄になっています。",[13,5259,5260],{},"という意味であり、サイトマップ上で新規のページが認識されているがサイト負荷が予想されたためクロール（インデックス登録）が見送られたということです。",[13,5262,5263],{},"この未登録状態を人力で解消するためには以下のようにします。",[13,5265,5266,5267],{},"1: 「検証」の行をクリックして表示される未登録のページ一覧のURLを確認\n",[74,5268],{":src":5269,":width":77},"'search-console-index-programmatically\u002Freslove1.png'",[13,5271,5272,5273],{},"2: URLを検査（一覧の虫眼鏡マーク）\n",[74,5274],{":src":5275,":width":77},"'search-console-index-programmatically\u002Freslove2.png'",[13,5277,5278],{},"3: 「インデックス登録をリクエスト」をクリック",[74,5280],{":src":5281,":width":77},"'search-console-index-programmatically\u002Freslove3.png'",[13,5283,5284],{},"リクエストを送ることでインデックス登録を再度行ってくれ、問題なければ登録されます。",[13,5286,5287,5288,5293],{},"一件ずつやってもいいのですが、検査やインデックスリクエストの時にローディングがかかるので200件あると結構時間がかかります。何かプログラム的にやる方法がないかを探したところ、",[53,5289,5292],{"href":5290,"rel":5291},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fapis\u002Findexing-api\u002Fv3\u002Fquickstart?hl=ja",[57],"Google Indexing API","というものがありました。",[46,5295,5216],{"id":5296},"apiを使用してプログラム的にリクエストする",[13,5298,5299,5303],{},[53,5300,5292],{"href":5301,"rel":5302},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fapis\u002Findexing-api\u002Fv3\u002Fquickstart",[57],"でおおまかな内容が書いてありますが、要約すると",[398,5305,5306,5309,5312],{},[20,5307,5308],{},"Google Cloud Platoform にてサービスアカウントを作成し、キーを取得する。（GCPのアカウントが必要です）",[20,5310,5311],{},"登録したアカウントのアドレスをSearch Consoleのユーザーに登録し、所有者と同じ権限を持たせる。",[20,5313,5314],{},"キーを用いたOauthでトークンを取得した後、対象のURLを更新するAPIリクエストを投げる。",[13,5316,5317],{},"上記の通りです。",[419,5319,5320],{"id":5320},"サービスアカウントの作成とキーの取得",[13,5322,5323],{},"GCPのアカウントがない場合はその作成から行ってください。（今回は省略）",[13,5325,5326],{},"「IAMと管理」から「サービスアカウント」に移動します。「サービスアカウントを作成」をクリックします。",[74,5328],{":src":5329,":width":77},"'search-console-index-programmatically\u002Fcreate-service-account.png'",[13,5331,5332],{},"アカウント名、IDを入力します。2と3は何も入力しなくて大丈夫です。",[13,5334,5335],{},"作成後に一覧に戻るので、作成したアカウントを詳細表示します。「キー」のタブに移動して「鍵を追加」から「新しい鍵を作成」をクリックします。",[74,5337],{":src":5338,":width":77},"'search-console-index-programmatically\u002Fcreate-key.png'",[13,5340,5341],{},"タイプでは「JSON」を選択し、作成をクリックします。JSONがダウンロードされますので、APIリクエストを行うディレクトリに保存しておいてください。まずはキーの取得が完了しました。",[419,5343,5345],{"id":5344},"サービスアカウントをserach-consoleに登録する","サービスアカウントをserach consoleに登録する",[13,5347,5348],{},"サービスアカウントがserach consoleを触れるようにユーザーとして登録しておきます。",[671,5350,5351],{},[13,5352,5353],{},"公式のドキュメントが旧UIを参照した形で書かれていますので注意してください。このページでは2024年1月現在のUIで説明します。",[13,5355,5356],{},"まずは権限を付与したいサイト（プロパティー）を選択し、「設定」をクリックします。表示されたらユーザーと権限をクリックします。",[74,5358],{":src":5359,":width":77},"'search-console-index-programmatically\u002Fsearch-console-user.png'",[13,5361,5362],{},"ユーザーと権限で「ユーザーを追加」をクリックして、サービスアカウントのアドレスとコピペして権限を「オーナー（委任された所有者）」を選択します。",[74,5364],{":src":5365,":width":77},"'search-console-index-programmatically\u002Fuser-auth.png'",[74,5367],{":src":5368,":width":77},"'search-console-index-programmatically\u002Fadd-user.png'",[13,5370,5371],{},"サービスアカウントのアドレスはサービスアカウント一覧または、キーJSONのclient_emailから取得できます。権限を付与してアドレスを登録すればこのアカウントからAPIアクセスができるようになります。（アドレス、所有者の確認は不要）",[419,5373,5375],{"id":5374},"apiリクエストの実装","APIリクエストの実装",[13,5377,5378,5379,5384],{},"私はnode.jsで実装しました。google apiライブラリを使用して簡単に実装ができ、",[53,5380,5383],{"href":5381,"rel":5382},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fapis\u002Findexing-api\u002Fv3\u002Fprereqs",[57],"公式にもいくつかの言語でサンプルコード","があります。",[13,5386,5387],{},"サンプルコードは1件しかできないので、ダウンロードした未登録のURLCSVファイルを読み込んで必要分をAPIリクエストするようなコードにしました。（本来であればBatchリクエストがいいですが、難しかったので今回は1件ずつリクエストを送ります。機会があればBatchリクエストの解説もします）",[13,5389,5390],{},"またIndexing APIは200件ほど（多分）のレートリミットがあるのでリクエスト間と送信数は時間を明けた方がいいです。とりあえずコードは以下の通りです。",[137,5392,5394],{"className":3069,"code":5393,"language":3071,"meta":142,"style":142},"\nvar fs = require('fs');\nvar axios = require('axios');\nvar { google } = require('googleapis');\nvar key = require('.\u002Fkey.json');\nvar {parse} = require('csv-parse\u002Fsync');\nvar {stringify} = require('csv-stringify\u002Fsync');\n\nconst jwtClient = new google.auth.JWT(\n  key.client_email,\n  null,\n  key.private_key,\n  ['https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Findexing'],\n  null\n);\n\njwtClient.authorize(function(err,token) {\n  if (err) {\n    console.log(err);\n    return;\n  }\n\n  \u002F\u002F CSVファイルからURLを読み込む\n  var urls = parse(fs.readFileSync('.\u002Fdata.csv'), {\n    columns: false,\n    trim: true\n  });\n\n  var requestedUrls = [];\n\n  \u002F\u002F 各URLに対してリクエストを送信\n  let stop = false;\n  (async () => {\n    for (let i = 0; i \u003C urls.length; i++) {\n      if(stop) break;\n      let url = urls[i][0];\n      await new Promise(resolve => setTimeout(resolve, 1000)); \u002F\u002F 1秒の遅延\n\n      await axios.post(\"https:\u002F\u002Findexing.googleapis.com\u002Fv3\u002FurlNotifications:publish\",{\n        'url': url,\n        'type': 'URL_UPDATED'\n      },{\n        headers: {\n          'Content-Type': 'application\u002Fjson',\n          'Authorization': 'Bearer '+token.access_token\n        },\n      })\n      .then(res=>{\n        requestedUrls.push([url]);\n      })\n      .catch(err=>{\n        console.error(err)\n        stop = true;\n      })\n    }\n\n    \u002F\u002F 成功したURLを requested.csv に書き込む\n    fs.writeFileSync('requested.csv', stringify(requestedUrls));\n  })();\n});\n\n",[102,5395,5396,5400,5426,5450,5478,5502,5530,5558,5562,5588,5600,5605,5616,5632,5637,5643,5647,5676,5689,5706,5713,5718,5722,5727,5764,5777,5787,5796,5800,5813,5817,5822,5836,5851,5890,5907,5931,5968,5972,5996,6012,6030,6035,6044,6065,6093,6098,6105,6123,6141,6147,6162,6178,6190,6196,6200,6204,6209,6242,6251],{"__ignoreMap":142},[146,5397,5398],{"class":148,"line":149},[146,5399,1082],{"emptyLinePlaceholder":936},[146,5401,5402,5405,5408,5410,5413,5415,5417,5420,5422,5424],{"class":148,"line":156},[146,5403,5404],{"class":162},"var",[146,5406,5407],{"class":866}," fs ",[146,5409,1093],{"class":152},[146,5411,5412],{"class":1268}," require",[146,5414,1360],{"class":866},[146,5416,1049],{"class":152},[146,5418,5419],{"class":175},"fs",[146,5421,1049],{"class":152},[146,5423,1384],{"class":866},[146,5425,1052],{"class":152},[146,5427,5428,5430,5433,5435,5437,5439,5441,5444,5446,5448],{"class":148,"line":184},[146,5429,5404],{"class":162},[146,5431,5432],{"class":866}," axios ",[146,5434,1093],{"class":152},[146,5436,5412],{"class":1268},[146,5438,1360],{"class":866},[146,5440,1049],{"class":152},[146,5442,5443],{"class":175},"axios",[146,5445,1049],{"class":152},[146,5447,1384],{"class":866},[146,5449,1052],{"class":152},[146,5451,5452,5454,5456,5459,5461,5463,5465,5467,5469,5472,5474,5476],{"class":148,"line":199},[146,5453,5404],{"class":162},[146,5455,1059],{"class":152},[146,5457,5458],{"class":866}," google ",[146,5460,1733],{"class":152},[146,5462,1209],{"class":152},[146,5464,5412],{"class":1268},[146,5466,1360],{"class":866},[146,5468,1049],{"class":152},[146,5470,5471],{"class":175},"googleapis",[146,5473,1049],{"class":152},[146,5475,1384],{"class":866},[146,5477,1052],{"class":152},[146,5479,5480,5482,5485,5487,5489,5491,5493,5496,5498,5500],{"class":148,"line":205},[146,5481,5404],{"class":162},[146,5483,5484],{"class":866}," key ",[146,5486,1093],{"class":152},[146,5488,5412],{"class":1268},[146,5490,1360],{"class":866},[146,5492,1049],{"class":152},[146,5494,5495],{"class":175},".\u002Fkey.json",[146,5497,1049],{"class":152},[146,5499,1384],{"class":866},[146,5501,1052],{"class":152},[146,5503,5504,5506,5508,5511,5513,5515,5517,5519,5521,5524,5526,5528],{"class":148,"line":228},[146,5505,5404],{"class":162},[146,5507,1059],{"class":152},[146,5509,5510],{"class":866},"parse",[146,5512,1733],{"class":152},[146,5514,1209],{"class":152},[146,5516,5412],{"class":1268},[146,5518,1360],{"class":866},[146,5520,1049],{"class":152},[146,5522,5523],{"class":175},"csv-parse\u002Fsync",[146,5525,1049],{"class":152},[146,5527,1384],{"class":866},[146,5529,1052],{"class":152},[146,5531,5532,5534,5536,5539,5541,5543,5545,5547,5549,5552,5554,5556],{"class":148,"line":249},[146,5533,5404],{"class":162},[146,5535,1059],{"class":152},[146,5537,5538],{"class":866},"stringify",[146,5540,1733],{"class":152},[146,5542,1209],{"class":152},[146,5544,5412],{"class":1268},[146,5546,1360],{"class":866},[146,5548,1049],{"class":152},[146,5550,5551],{"class":175},"csv-stringify\u002Fsync",[146,5553,1049],{"class":152},[146,5555,1384],{"class":866},[146,5557,1052],{"class":152},[146,5559,5560],{"class":148,"line":270},[146,5561,1082],{"emptyLinePlaceholder":936},[146,5563,5564,5566,5569,5571,5573,5576,5578,5581,5583,5586],{"class":148,"line":284},[146,5565,1087],{"class":162},[146,5567,5568],{"class":866}," jwtClient ",[146,5570,1093],{"class":152},[146,5572,4504],{"class":152},[146,5574,5575],{"class":866}," google",[146,5577,1176],{"class":152},[146,5579,5580],{"class":866},"auth",[146,5582,1176],{"class":152},[146,5584,5585],{"class":1268},"JWT",[146,5587,1807],{"class":866},[146,5589,5590,5593,5595,5598],{"class":148,"line":296},[146,5591,5592],{"class":866},"  key",[146,5594,1176],{"class":152},[146,5596,5597],{"class":866},"client_email",[146,5599,181],{"class":152},[146,5601,5602],{"class":148,"line":302},[146,5603,5604],{"class":152},"  null,\n",[146,5606,5607,5609,5611,5614],{"class":148,"line":316},[146,5608,5592],{"class":866},[146,5610,1176],{"class":152},[146,5612,5613],{"class":866},"private_key",[146,5615,181],{"class":152},[146,5617,5618,5621,5623,5626,5628,5630],{"class":148,"line":326},[146,5619,5620],{"class":866},"  [",[146,5622,1049],{"class":152},[146,5624,5625],{"class":175},"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Findexing",[146,5627,1049],{"class":152},[146,5629,1221],{"class":866},[146,5631,181],{"class":152},[146,5633,5634],{"class":148,"line":332},[146,5635,5636],{"class":152},"  null\n",[146,5638,5639,5641],{"class":148,"line":338},[146,5640,1384],{"class":866},[146,5642,1052],{"class":152},[146,5644,5645],{"class":148,"line":344},[146,5646,1082],{"emptyLinePlaceholder":936},[146,5648,5649,5652,5654,5657,5659,5662,5664,5667,5669,5672,5674],{"class":148,"line":1396},[146,5650,5651],{"class":866},"jwtClient",[146,5653,1176],{"class":152},[146,5655,5656],{"class":1268},"authorize",[146,5658,1360],{"class":866},[146,5660,5661],{"class":162},"function",[146,5663,1360],{"class":152},[146,5665,5666],{"class":1581},"err",[146,5668,1099],{"class":152},[146,5670,5671],{"class":1581},"token",[146,5673,1384],{"class":152},[146,5675,861],{"class":152},[146,5677,5678,5681,5683,5685,5687],{"class":148,"line":1420},[146,5679,5680],{"class":1027},"  if",[146,5682,1150],{"class":1214},[146,5684,5666],{"class":866},[146,5686,1196],{"class":1214},[146,5688,153],{"class":152},[146,5690,5691,5694,5696,5698,5700,5702,5704],{"class":148,"line":1425},[146,5692,5693],{"class":866},"    console",[146,5695,1176],{"class":152},[146,5697,1880],{"class":1268},[146,5699,1360],{"class":1214},[146,5701,5666],{"class":866},[146,5703,1384],{"class":1214},[146,5705,1052],{"class":152},[146,5707,5708,5711],{"class":148,"line":1431},[146,5709,5710],{"class":1027},"    return",[146,5712,1052],{"class":152},[146,5714,5715],{"class":148,"line":1488},[146,5716,5717],{"class":152},"  }\n",[146,5719,5720],{"class":148,"line":1518},[146,5721,1082],{"emptyLinePlaceholder":936},[146,5723,5724],{"class":148,"line":1527},[146,5725,5726],{"class":1141},"  \u002F\u002F CSVファイルからURLを読み込む\n",[146,5728,5729,5732,5735,5737,5740,5742,5744,5746,5749,5751,5753,5756,5758,5760,5762],{"class":148,"line":1533},[146,5730,5731],{"class":162},"  var",[146,5733,5734],{"class":866}," urls",[146,5736,1209],{"class":152},[146,5738,5739],{"class":1268}," parse",[146,5741,1360],{"class":1214},[146,5743,5419],{"class":866},[146,5745,1176],{"class":152},[146,5747,5748],{"class":1268},"readFileSync",[146,5750,1360],{"class":1214},[146,5752,1049],{"class":152},[146,5754,5755],{"class":175},".\u002Fdata.csv",[146,5757,1049],{"class":152},[146,5759,1384],{"class":1214},[146,5761,1099],{"class":152},[146,5763,861],{"class":152},[146,5765,5766,5769,5771,5775],{"class":148,"line":1566},[146,5767,5768],{"class":1214},"    columns",[146,5770,169],{"class":152},[146,5772,5774],{"class":5773},"sbqyR"," false",[146,5776,181],{"class":152},[146,5778,5779,5782,5784],{"class":148,"line":1592},[146,5780,5781],{"class":1214},"    trim",[146,5783,169],{"class":152},[146,5785,5786],{"class":5773}," true\n",[146,5788,5789,5792,5794],{"class":148,"line":1620},[146,5790,5791],{"class":152},"  }",[146,5793,1384],{"class":1214},[146,5795,1052],{"class":152},[146,5797,5798],{"class":148,"line":1630},[146,5799,1082],{"emptyLinePlaceholder":936},[146,5801,5802,5804,5807,5809,5811],{"class":148,"line":1636},[146,5803,5731],{"class":162},[146,5805,5806],{"class":866}," requestedUrls",[146,5808,1209],{"class":152},[146,5810,1130],{"class":1214},[146,5812,1052],{"class":152},[146,5814,5815],{"class":148,"line":1641},[146,5816,1082],{"emptyLinePlaceholder":936},[146,5818,5819],{"class":148,"line":1646},[146,5820,5821],{"class":1141},"  \u002F\u002F 各URLに対してリクエストを送信\n",[146,5823,5824,5827,5830,5832,5834],{"class":148,"line":1658},[146,5825,5826],{"class":162},"  let",[146,5828,5829],{"class":866}," stop",[146,5831,1209],{"class":152},[146,5833,5774],{"class":5773},[146,5835,1052],{"class":152},[146,5837,5838,5841,5844,5847,5849],{"class":148,"line":1675},[146,5839,5840],{"class":1214},"  (",[146,5842,5843],{"class":162},"async",[146,5845,5846],{"class":152}," ()",[146,5848,1587],{"class":162},[146,5850,861],{"class":152},[146,5852,5853,5856,5858,5860,5862,5864,5866,5868,5870,5873,5875,5877,5880,5882,5884,5886,5888],{"class":148,"line":1686},[146,5854,5855],{"class":1027},"    for",[146,5857,1150],{"class":1214},[146,5859,1153],{"class":162},[146,5861,1190],{"class":866},[146,5863,1209],{"class":152},[146,5865,1162],{"class":1161},[146,5867,1165],{"class":152},[146,5869,1190],{"class":866},[146,5871,5872],{"class":152}," \u003C",[146,5874,5734],{"class":866},[146,5876,1176],{"class":152},[146,5878,5879],{"class":866},"length",[146,5881,1165],{"class":152},[146,5883,1190],{"class":866},[146,5885,1193],{"class":152},[146,5887,1196],{"class":1214},[146,5889,153],{"class":152},[146,5891,5892,5895,5897,5900,5902,5905],{"class":148,"line":1696},[146,5893,5894],{"class":1027},"      if",[146,5896,1360],{"class":1214},[146,5898,5899],{"class":866},"stop",[146,5901,1196],{"class":1214},[146,5903,5904],{"class":1027},"break",[146,5906,1052],{"class":152},[146,5908,5909,5912,5915,5917,5919,5921,5923,5925,5927,5929],{"class":148,"line":1713},[146,5910,5911],{"class":162},"      let",[146,5913,5914],{"class":866}," url",[146,5916,1209],{"class":152},[146,5918,5734],{"class":866},[146,5920,1215],{"class":1214},[146,5922,1218],{"class":866},[146,5924,1478],{"class":1214},[146,5926,1291],{"class":1161},[146,5928,1221],{"class":1214},[146,5930,1052],{"class":152},[146,5932,5933,5936,5938,5941,5943,5946,5948,5951,5953,5955,5957,5960,5963,5965],{"class":148,"line":1724},[146,5934,5935],{"class":1027},"      await",[146,5937,4504],{"class":152},[146,5939,5940],{"class":211}," Promise",[146,5942,1360],{"class":1214},[146,5944,5945],{"class":1581},"resolve",[146,5947,1587],{"class":162},[146,5949,5950],{"class":1268}," setTimeout",[146,5952,1360],{"class":1214},[146,5954,5945],{"class":866},[146,5956,1099],{"class":152},[146,5958,5959],{"class":1161}," 1000",[146,5961,5962],{"class":1214},"))",[146,5964,1165],{"class":152},[146,5966,5967],{"class":1141}," \u002F\u002F 1秒の遅延\n",[146,5969,5970],{"class":148,"line":1730},[146,5971,1082],{"emptyLinePlaceholder":936},[146,5973,5974,5976,5979,5981,5984,5986,5988,5991,5993],{"class":148,"line":1742},[146,5975,5935],{"class":1027},[146,5977,5978],{"class":866}," axios",[146,5980,1176],{"class":152},[146,5982,5983],{"class":1268},"post",[146,5985,1360],{"class":1214},[146,5987,166],{"class":152},[146,5989,5990],{"class":175},"https:\u002F\u002Findexing.googleapis.com\u002Fv3\u002FurlNotifications:publish",[146,5992,166],{"class":152},[146,5994,5995],{"class":152},",{\n",[146,5997,5998,6001,6004,6006,6008,6010],{"class":148,"line":1747},[146,5999,6000],{"class":152},"        '",[146,6002,6003],{"class":1214},"url",[146,6005,1049],{"class":152},[146,6007,169],{"class":152},[146,6009,5914],{"class":866},[146,6011,181],{"class":152},[146,6013,6014,6016,6019,6021,6023,6025,6028],{"class":148,"line":2041},[146,6015,6000],{"class":152},[146,6017,6018],{"class":1214},"type",[146,6020,1049],{"class":152},[146,6022,169],{"class":152},[146,6024,1043],{"class":152},[146,6026,6027],{"class":175},"URL_UPDATED",[146,6029,2979],{"class":152},[146,6031,6032],{"class":148,"line":2047},[146,6033,6034],{"class":152},"      },{\n",[146,6036,6037,6040,6042],{"class":148,"line":4},[146,6038,6039],{"class":1214},"        headers",[146,6041,169],{"class":152},[146,6043,861],{"class":152},[146,6045,6046,6049,6052,6054,6056,6058,6061,6063],{"class":148,"line":2058},[146,6047,6048],{"class":152},"          '",[146,6050,6051],{"class":1214},"Content-Type",[146,6053,1049],{"class":152},[146,6055,169],{"class":152},[146,6057,1043],{"class":152},[146,6059,6060],{"class":175},"application\u002Fjson",[146,6062,1049],{"class":152},[146,6064,181],{"class":152},[146,6066,6067,6069,6072,6074,6076,6078,6081,6083,6086,6088,6090],{"class":148,"line":2063},[146,6068,6048],{"class":152},[146,6070,6071],{"class":1214},"Authorization",[146,6073,1049],{"class":152},[146,6075,169],{"class":152},[146,6077,1043],{"class":152},[146,6079,6080],{"class":175},"Bearer ",[146,6082,1049],{"class":152},[146,6084,6085],{"class":152},"+",[146,6087,5671],{"class":866},[146,6089,1176],{"class":152},[146,6091,6092],{"class":866},"access_token\n",[146,6094,6095],{"class":148,"line":2081},[146,6096,6097],{"class":152},"        },\n",[146,6099,6100,6102],{"class":148,"line":2098},[146,6101,4574],{"class":152},[146,6103,6104],{"class":1214},")\n",[146,6106,6107,6110,6113,6115,6118,6121],{"class":148,"line":2120},[146,6108,6109],{"class":152},"      .",[146,6111,6112],{"class":1268},"then",[146,6114,1360],{"class":1214},[146,6116,6117],{"class":1581},"res",[146,6119,6120],{"class":162},"=>",[146,6122,153],{"class":152},[146,6124,6125,6128,6130,6132,6134,6136,6139],{"class":148,"line":2133},[146,6126,6127],{"class":866},"        requestedUrls",[146,6129,1176],{"class":152},[146,6131,1496],{"class":1268},[146,6133,1272],{"class":1214},[146,6135,6003],{"class":866},[146,6137,6138],{"class":1214},"])",[146,6140,1052],{"class":152},[146,6142,6143,6145],{"class":148,"line":2140},[146,6144,4574],{"class":152},[146,6146,6104],{"class":1214},[146,6148,6149,6151,6154,6156,6158,6160],{"class":148,"line":2145},[146,6150,6109],{"class":152},[146,6152,6153],{"class":1268},"catch",[146,6155,1360],{"class":1214},[146,6157,5666],{"class":1581},[146,6159,6120],{"class":162},[146,6161,153],{"class":152},[146,6163,6164,6167,6169,6172,6174,6176],{"class":148,"line":2157},[146,6165,6166],{"class":866},"        console",[146,6168,1176],{"class":152},[146,6170,6171],{"class":1268},"error",[146,6173,1360],{"class":1214},[146,6175,5666],{"class":866},[146,6177,6104],{"class":1214},[146,6179,6180,6183,6185,6188],{"class":148,"line":2163},[146,6181,6182],{"class":866},"        stop",[146,6184,1209],{"class":152},[146,6186,6187],{"class":5773}," true",[146,6189,1052],{"class":152},[146,6191,6192,6194],{"class":148,"line":2169},[146,6193,4574],{"class":152},[146,6195,6104],{"class":1214},[146,6197,6198],{"class":148,"line":2175},[146,6199,1633],{"class":152},[146,6201,6202],{"class":148,"line":2181},[146,6203,1082],{"emptyLinePlaceholder":936},[146,6205,6206],{"class":148,"line":2187},[146,6207,6208],{"class":1141},"    \u002F\u002F 成功したURLを requested.csv に書き込む\n",[146,6210,6211,6214,6216,6219,6221,6223,6226,6228,6230,6233,6235,6238,6240],{"class":148,"line":2193},[146,6212,6213],{"class":866},"    fs",[146,6215,1176],{"class":152},[146,6217,6218],{"class":1268},"writeFileSync",[146,6220,1360],{"class":1214},[146,6222,1049],{"class":152},[146,6224,6225],{"class":175},"requested.csv",[146,6227,1049],{"class":152},[146,6229,1099],{"class":152},[146,6231,6232],{"class":1268}," stringify",[146,6234,1360],{"class":1214},[146,6236,6237],{"class":866},"requestedUrls",[146,6239,5962],{"class":1214},[146,6241,1052],{"class":152},[146,6243,6244,6246,6249],{"class":148,"line":2199},[146,6245,5791],{"class":152},[146,6247,6248],{"class":1214},")()",[146,6250,1052],{"class":152},[146,6252,6253,6255,6257],{"class":148,"line":2205},[146,6254,1733],{"class":152},[146,6256,1384],{"class":866},[146,6258,1052],{"class":152},[13,6260,6261],{},"更新が行われたかを確認したいときは、対象のURLをserach consoleで検査したときに「ページのインデックス登録」のタブでクロールされたか、インデックス登録を許可が「はい」になっているかで確認できます。",[13,6263,6264],{},"もし上記の方法でインデックスされない場合はページの品質が悪いか、URLで変なリダイレクトが起きていたり、正常に表示できていない可能性がありますのでサーバの設定などを見直してみてください。",[13,6266,6267],{},"私の方でもなんとか200件のインデックスが行われました。（右側の緑が急激に増えている）",[74,6269],{":src":5234,":width":77},[908,6271,6272],{},"html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .s7ZW3, html code.shiki .s7ZW3{--shiki-default:#BABED8;--shiki-default-font-style:italic}html pre.shiki code .s6cf3, html code.shiki .s6cf3{--shiki-default:#89DDFF;--shiki-default-font-style:italic}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .sbqyR, html code.shiki .sbqyR{--shiki-default:#FF9CAC}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":142,"searchDepth":184,"depth":184,"links":6274},[6275,6276],{"id":5220,"depth":156,"text":5221},{"id":5296,"depth":156,"text":5216,"children":6277},[6278,6279,6280],{"id":5320,"depth":184,"text":5320},{"id":5344,"depth":184,"text":5345},{"id":5374,"depth":184,"text":5375},[930],"2023-12-31","Google Search ConsoleのインデックスリクエストをAPI通じて行う",{},"\u002Farticles\u002Fsearch-console-index-programmatically",{"title":5207,"description":6283},"articles\u002Fsearch-console-index-programmatically",[],"KKGIUM7ALrbkGgHWiRqk1CA0kYCz6TJfovN44yVxLL0",{"id":6291,"title":6292,"body":6293,"category":6354,"createdAt":6355,"description":6356,"extension":933,"index":934,"meta":6357,"navigation":936,"path":6358,"publish":936,"seo":6359,"series":934,"seriesTitle":934,"stem":6360,"tag":6361,"thumbnail":6363,"updatedAt":934,"__hash__":6364},"articles\u002Farticles\u002Fknow-users-background.md","顧客からの要望は必ず背景を聞こう。",{"type":10,"value":6294,"toc":6349},[6295,6298,6301,6306,6309,6312,6315,6318,6321,6324,6327,6330,6333,6336,6343,6346],[46,6296,6297],{"id":6297},"国語の問題",[13,6299,6300],{},"ある日、担当ディレクターとして完成したwebサイトのお客様から以下の様な連絡をもらいました。",[671,6302,6303],{},[13,6304,6305],{},"サイトにアクセスカウンターを設置したいのですが、可能でしょうか？",[13,6307,6308],{},"実際のメールはもう少しいろいろありましたが、要望は上記の通りでした。",[13,6310,6311],{},"アクセスカウンターというと懐かしいやつです。サイトの下部にあって訪問者数を記録して、キリ番だと嬉しいあれです。しかし開発者的には結構面倒です。ユーザーのリクエスト数をDBなりファイルなりで保存して、都度それを表示しないといけません。",[13,6313,6314],{},"さらにいうとそのサイトは「静的サイト」（正確にはHTMLを出力している）であるため、アクセスカウンターの様な動的な機能の実装が非常に面倒でした。",[13,6316,6317],{},"あれこれ考えていましたが、ひとまず先方に電話を入れて確認をしました。そこで上記のメールではなかった「なぜアクセスカウンターをつけたいのか」という背景について聞いたところ、先方は「サイトの訪問者数を知りたい」と答えました。",[13,6319,6320],{},"この場合、サイトにはGoogle Analyticsが入っているためページごとの細かい閲覧数などを確認できる旨を伝えたところ、先方は満足して今回の要望はクローズしました。",[46,6322,6323],{"id":6323},"ここで気をつけたいこと",[13,6325,6326],{},"このことで気をつけたいことは「顧客から要望をもらった時、その背景や理由については必ず把握しましょう」ということです。先方からもらったメールには「要望＝アクセスカウンターを設置したい」ということのみが書かれており、「なぜアクセスカウンターを設置するに至ったのかの理由」がありませんでした。",[13,6328,6329],{},"また自分はアクセスカウンターと聞いた瞬間、静的サイトなのでどうやって実装しようかと考え込み始めました。（悪い癖）\nしかし先方の真意は「サイトの訪問者数を把握する」という「データとして利用するためのアクセスカウンターの設置」であり「コンテンツとしてのアクセスカウンターの設置」ではありません。しかし自分は過去の経験やITにいる立場上、後者として認識していまい見事にお客様と認識のずれが発生しました。",[13,6331,6332],{},"よくよく見て理由がないことに気づいて、電話で詳細に聞いたため今回は特に問題ありませんでした。",[46,6334,6335],{"id":6335},"顧客は要望しか言わない",[13,6337,6338,6339,6342],{},"お客様から来る要望は上記の様に",[82,6340,6341],{},"背景や理由が抜けていることが多いです。"," しかしこれは、お客様は開発者でないため専門的な知見や知識がないため仕方ありません。または上手く言語化して伝えられないということもあります。ここをうまく読み取るのが開発者やディレクターの仕事です。",[13,6344,6345],{},"実際の業務や要望を実現するにあたり、開発者は顧客からの要望の背景と理由を確認する様にしましょう。素晴らしいお客様は最初から理由をつけて話してくれることもありますが、大体はすっぽ抜けています。理由や背景を確認することで、開発者と顧客の認識のズレをなくすとともにより有効的な解決策や提案に発展することが多いです。",[13,6347,6348],{},"文字通りの内容でなく、きちんと要望や要求に対してはその背景と理由を尋ねて認識を合わせる様にしましょう。",{"title":142,"searchDepth":184,"depth":184,"links":6350},[6351,6352,6353],{"id":6297,"depth":156,"text":6297},{"id":6323,"depth":156,"text":6323},{"id":6335,"depth":156,"text":6335},[930],"2023-02-01","アクセスカウンターをサイトに設置したいお客様の声",{},"\u002Farticles\u002Fknow-users-background",{"title":6292,"description":6356},"articles\u002Fknow-users-background",[6362],"direction","know-users-background\u002Fthumbnail.png","ijGmli97ihKnMk7TPk-upb6BQKjTQntVDe3CasufJYU",{"id":6366,"title":6367,"body":6368,"category":7685,"createdAt":7686,"description":7687,"extension":933,"index":149,"meta":7688,"navigation":936,"path":7689,"publish":936,"seo":7690,"series":5197,"seriesTitle":5198,"stem":7691,"tag":7692,"thumbnail":7693,"updatedAt":934,"__hash__":7694},"series\u002Fseries\u002Frtmp-manager-server-1.md","配信中継サーバ開発1：ローカルのDockerとNginxを用いてGoproからRTMPで送信されたライブ配信映像を再生する",{"type":10,"value":6369,"toc":7658},[6370,6374,6377,6380,6383,6386,6397,6400,6403,6414,6421,6423,6426,6429,6432,6435,6438,6441,6444,6455,6458,6469,6473,6476,6480,6487,6491,6494,6507,6513,6516,6531,6540,6545,6640,6643,6646,6649,6652,6870,6877,6880,6961,6967,6970,6977,7159,7162,7165,7171,7508,7514,7517,7523,7526,7530,7534,7537,7548,7551,7554,7557,7569,7575,7581,7590,7593,7596,7599,7603,7609,7613,7620,7623,7626,7629,7631,7634,7656],[39,6371,6373],{"className":6372},[42,594],"\nこの記事はもともと[https:\u002F\u002Fjun-app.com\u002Farticles\u002Frtmp-docker-local](https:\u002F\u002Fjun-app.com\u002Farticles\u002Frtmp-docker-local) にて掲載していました。この記事に関連する内容をシリーズ化するため、このURLに移動しました。\n",[13,6375,6376],{},"こんにちはjunです。今回の記事は一風変わって物理世界をちょっと扱います。いつもはPC内で行えることばかりやっていますが、今回はGoproをつかってタイトルの通りライブ映像を再生したいと思います。RTMPサーバーをローカルで作成し、そこに送信できるかのテストをしてみたかった感じです。",[13,6378,6379],{},"ローカルPCのDockerにNignxをインストールしてRTMPの設定をしてブラウザから見れるようにします。この記事ではまずなぜ、このようなことを始めたいのかという理由から述べます。実装内容をさっさと知りたい方は「RTMPサーバー実装」へ移動してください。",[46,6381,6382],{"id":6382},"事の経緯",[13,6384,6385],{},"経緯としては私がyoutubeライブをやりたくなったからです笑。単純にyoutubeライブをやりたいだけならば、さっさとwebカメラからやればいいのですが、私はサイクリングが趣味でなのでその風景をライブしたいと思っていました。持っているアクションカメラがGoproであり、一応公式のスマホアプリを使用することでyoutubeライブをすることができるのですが以下の懸念がありました。",[17,6387,6388,6391,6394],{},[20,6389,6390],{},"Gopro、スマホアプリなどカメラ側の不具合によるライブの中断",[20,6392,6393],{},"柔軟なカメラ切り替えを行える環境の準備",[20,6395,6396],{},"スマホからのyoutubeライブ配信は登録者が50人必要（１番の障壁です笑）",[419,6398,6399],{"id":6399},"カメラ側によるライブの中断",[13,6401,6402],{},"Goproは簡単にライブ配信ができるのですが、以下の様なカメラ・スマホ側の要因によってライブが中断される可能性があります。",[17,6404,6405,6408,6411],{},[20,6406,6407],{},"Goproの熱暴走",[20,6409,6410],{},"トンネルなどでwi-fi・ネットワーク切断",[20,6412,6413],{},"カメラ、スマホの電池切れ",[13,6415,6416,6417,6420],{},"本当に中断されるかは ",[82,6418,6419],{},"未検証"," ですが可能性があるのでこれを避けるためには「Gopro側からライブの設定を制御するのでなく、Goproからは音声と映像を取得するのみで配信は別環境で行う」ことがベストな気がします。",[419,6422,6393],{"id":6393},[13,6424,6425],{},"ライブ配信では映しっぱなしでもいいのですが、撮影禁止の箇所だったり環境によって映像のON\u002FOFFを切り替えたいと思っています。Goproアプリは確かにライブ配信は簡単なのですが、より細かい設定や制御は実装されていません。",[419,6427,6428],{"id":6428},"登録者が50人必要",[13,6430,6431],{},"スマホ通じてライブ配信をする場合はどうやら50人が必要でしたが、RTMPやPCからであれば0人でもOKでした。",[13,6433,6434],{},"登録者的な問題だったり、より細かい制御を実装するために以下のような構成を考えた結果、まずは自前RTMPサーバーへの映像の送信と再生ができるかをテストしてみたかった次第です。最終的にはRTMPサーバーから映像を取得してyoutubeに流します。サーバーでは映像の表示制御やyoutubeのコメント読み上げなど配信に関係する機能を管理するものとします。",[74,6436],{":src":6437,":width":77},"'rtmp-docker-local\u002Ffig.png'",[46,6439,6440],{"id":6440},"実装概要",[13,6442,6443],{},"この記事での目標は",[398,6445,6446,6449,6452],{},[20,6447,6448],{},"Goproからの映像をアプリを通じてコンテナ内のWebサーバーにRTMP通信で映像を送信。",[20,6450,6451],{},"映像をドキュメントルートへ配置。",[20,6453,6454],{},"webブラウザからアクセスして映像を見れうようにする。",[13,6456,6457],{},"まずはRTMPサーバーの実装を行います。使用しているOSはmacOs Catalina 10.15.5でdockerは3.5.2です。実装の手順としては",[398,6459,6460,6463,6466],{},[20,6461,6462],{},"Dockerを用いてRTMPとwebが可能なNginxを構築する。",[20,6464,6465],{},"RTMPの設定をする。",[20,6467,6468],{},"web側でHLSにて映像をvideoタグを用いて再生する。",[419,6470,6472],{"id":6471},"rtmpとは","RTMPとは？",[13,6474,6475],{},"ここまで出ているRTMPとはReal Time Messaging ProtocolのことでTCP上のプロトコルで映像、音声、データを細かいフラグメントに分けてストリーミング送信ができます。今回のようなリアルタイムに映像を撮影してサーバーに送信する際にも利用されます。",[419,6477,6479],{"id":6478},"なぜnginx","なぜNginx？",[13,6481,6482,6483,6486],{},"NginxではRTMPモジュールというのがあり、インストールしてconfファイルに少し記述するだけで1935ポートに",[102,6484,6485],{},"rtmp:\u002F\u002Fexample.com\u002Flive","のようなURLでサーバーに送信できます。",[46,6488,6490],{"id":6489},"rtmpサーバー実装","RTMPサーバー実装",[419,6492,6493],{"id":6493},"docker-composeの作成",[13,6495,6496,6497,6500,6501,6503,6504,6506],{},"では早速やっていきましょう。",[102,6498,6499],{},"rtmptest","みたいな適当なディレクトリを作成して、",[102,6502,3299],{},"と",[102,6505,3361],{},"ファイルを作成します。また、webとconfファイルを格納してボリュームしておくディレクトリも作成します。",[137,6508,6511],{"className":6509,"code":6510,"language":3289},[3287],"mkdir rtmptest\ncd rtmptest\n\ntouch Dockerfile docker-compose.yml\nmkdir html conf\n",[102,6512,6510],{"__ignoreMap":142},[13,6514,6515],{},"Dockerfileは以下のようにしておきます。",[137,6517,6519],{"className":3297,"code":6518,"language":3299,"meta":142,"style":142},"FROM tiangolo\u002Fnginx-rtmp\nVOLUME [\"\u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\",\"\u002Fetc\u002Fnginx\"]\n",[102,6520,6521,6526],{"__ignoreMap":142},[146,6522,6523],{"class":148,"line":149},[146,6524,6525],{},"FROM tiangolo\u002Fnginx-rtmp\n",[146,6527,6528],{"class":148,"line":156},[146,6529,6530],{},"VOLUME [\"\u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\",\"\u002Fetc\u002Fnginx\"]\n",[13,6532,6533,6534,6539],{},"今回は",[53,6535,6538],{"href":6536,"rel":6537},"https:\u002F\u002Fhub.docker.com\u002Fr\u002Ftiangolo\u002Fnginx-rtmp\u002F",[57],"rtmpの設定があるnginxのイメージ","を利用します。",[13,6541,6542,6544],{},[102,6543,3361],{},"は以下の通りです。",[137,6546,6551],{"className":6547,"code":6548,"filename":6549,"language":6550,"meta":142,"style":142},"language-yaml shiki shiki-themes material-theme-ocean","version: '2'\nservices:\n  app:\n    build: .\n    ports:\n      - \"8085:80\"\n      - \"1935:1935\"\n    volumes:\n      -  .\u002Fhtml:\u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\n      -  .\u002Fconf\u002Fnginx.conf:\u002Fetc\u002Fnginx\u002Fnginx.conf\n      -  .\u002Fconf\u002Fdefault.conf:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf\n","docke-compose.yml","yaml",[102,6552,6553,6565,6571,6578,6586,6592,6603,6613,6619,6626,6633],{"__ignoreMap":142},[146,6554,6555,6557,6559,6561,6563],{"class":148,"line":149},[146,6556,3369],{"class":1214},[146,6558,169],{"class":152},[146,6560,1043],{"class":152},[146,6562,3376],{"class":175},[146,6564,2979],{"class":152},[146,6566,6567,6569],{"class":148,"line":156},[146,6568,3383],{"class":1214},[146,6570,3386],{"class":152},[146,6572,6573,6576],{"class":148,"line":184},[146,6574,6575],{"class":1214},"  app",[146,6577,3386],{"class":152},[146,6579,6580,6582,6584],{"class":148,"line":199},[146,6581,3524],{"class":1214},[146,6583,169],{"class":152},[146,6585,3529],{"class":1161},[146,6587,6588,6590],{"class":148,"line":205},[146,6589,3408],{"class":1214},[146,6591,3386],{"class":152},[146,6593,6594,6596,6598,6601],{"class":148,"line":228},[146,6595,3415],{"class":152},[146,6597,172],{"class":152},[146,6599,6600],{"class":175},"8085:80",[146,6602,293],{"class":152},[146,6604,6605,6607,6609,6611],{"class":148,"line":249},[146,6606,3415],{"class":152},[146,6608,172],{"class":152},[146,6610,3491],{"class":175},[146,6612,293],{"class":152},[146,6614,6615,6617],{"class":148,"line":270},[146,6616,3438],{"class":1214},[146,6618,3386],{"class":152},[146,6620,6621,6623],{"class":148,"line":284},[146,6622,3415],{"class":152},[146,6624,6625],{"class":175},"  .\u002Fhtml:\u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\n",[146,6627,6628,6630],{"class":148,"line":296},[146,6629,3415],{"class":152},[146,6631,6632],{"class":175},"  .\u002Fconf\u002Fnginx.conf:\u002Fetc\u002Fnginx\u002Fnginx.conf\n",[146,6634,6635,6637],{"class":148,"line":302},[146,6636,3415],{"class":152},[146,6638,6639],{"class":175},"  .\u002Fconf\u002Fdefault.conf:\u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf\n",[419,6641,6642],{"id":6642},"confファイル調整",[13,6644,6645],{},"nginxのconfファイルを調整してRTMPとwebアクセスができるようにします。",[819,6647,6648],{"id":3349},"RTMP",[13,6650,6651],{},"RTMPはnginx.confというapacheで言うhttp.confに以下のように記述します。",[137,6653,6655],{"className":3632,"code":6654,"filename":3634,"language":3635,"meta":142,"style":142},"worker_processes  auto;\n\nerror_log  \u002Fvar\u002Flog\u002Fnginx\u002Ferror.log notice;\npid        \u002Fvar\u002Frun\u002Fnginx.pid;\n\n\nevents {\n    worker_connections  1024;\n}\n\nrtmp_auto_push on;\n\nrtmp {\n    server {\n        listen 1935;\n        listen [::]:1935 ipv6only=on;\n        access_log \u002Fvar\u002Flog\u002Frtmp_access.log;\n        chunk_size 4096;\n        timeout 10s;\n\n        application live {\n            live on;\n\n            # HLSの記述欄\n            hls on;\n            # ここに映像ファイルが配置される\n            hls_path \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\u002Fhls;\n            hls_fragment 10s;\n        }\n    }\n}\n\n\n\nhttp {\n    include       \u002Fetc\u002Fnginx\u002Fmime.types;\n    default_type  application\u002Foctet-stream;\n\n    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log  \u002Fvar\u002Flog\u002Fnginx\u002Faccess.log  main;\n\n    sendfile        on;\n    #tcp_nopush     on;\n\n    keepalive_timeout  65;\n\n    #gzip  on;\n\n    include \u002Fetc\u002Fnginx\u002Fconf.d\u002F*.conf;\n}\n",[102,6656,6657,6661,6665,6669,6673,6677,6681,6685,6689,6693,6697,6701,6705,6709,6713,6717,6721,6725,6729,6733,6737,6741,6745,6749,6753,6757,6761,6766,6770,6774,6778,6782,6786,6790,6794,6798,6802,6806,6810,6814,6818,6822,6826,6830,6834,6838,6842,6846,6850,6854,6858,6862,6866],{"__ignoreMap":142},[146,6658,6659],{"class":148,"line":149},[146,6660,3642],{},[146,6662,6663],{"class":148,"line":156},[146,6664,1082],{"emptyLinePlaceholder":936},[146,6666,6667],{"class":148,"line":184},[146,6668,3651],{},[146,6670,6671],{"class":148,"line":199},[146,6672,3656],{},[146,6674,6675],{"class":148,"line":205},[146,6676,1082],{"emptyLinePlaceholder":936},[146,6678,6679],{"class":148,"line":228},[146,6680,1082],{"emptyLinePlaceholder":936},[146,6682,6683],{"class":148,"line":249},[146,6684,3669],{},[146,6686,6687],{"class":148,"line":270},[146,6688,3674],{},[146,6690,6691],{"class":148,"line":284},[146,6692,347],{},[146,6694,6695],{"class":148,"line":296},[146,6696,1082],{"emptyLinePlaceholder":936},[146,6698,6699],{"class":148,"line":302},[146,6700,3687],{},[146,6702,6703],{"class":148,"line":316},[146,6704,1082],{"emptyLinePlaceholder":936},[146,6706,6707],{"class":148,"line":326},[146,6708,3696],{},[146,6710,6711],{"class":148,"line":332},[146,6712,3701],{},[146,6714,6715],{"class":148,"line":338},[146,6716,3706],{},[146,6718,6719],{"class":148,"line":344},[146,6720,3711],{},[146,6722,6723],{"class":148,"line":1396},[146,6724,3716],{},[146,6726,6727],{"class":148,"line":1420},[146,6728,3721],{},[146,6730,6731],{"class":148,"line":1425},[146,6732,3726],{},[146,6734,6735],{"class":148,"line":1431},[146,6736,1082],{"emptyLinePlaceholder":936},[146,6738,6739],{"class":148,"line":1488},[146,6740,3735],{},[146,6742,6743],{"class":148,"line":1518},[146,6744,3740],{},[146,6746,6747],{"class":148,"line":1527},[146,6748,1082],{"emptyLinePlaceholder":936},[146,6750,6751],{"class":148,"line":1533},[146,6752,3749],{},[146,6754,6755],{"class":148,"line":1566},[146,6756,3754],{},[146,6758,6759],{"class":148,"line":1592},[146,6760,3759],{},[146,6762,6763],{"class":148,"line":1620},[146,6764,6765],{},"            hls_path \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\u002Fhls;\n",[146,6767,6768],{"class":148,"line":1630},[146,6769,3769],{},[146,6771,6772],{"class":148,"line":1636},[146,6773,335],{},[146,6775,6776],{"class":148,"line":1641},[146,6777,1633],{},[146,6779,6780],{"class":148,"line":1646},[146,6781,347],{},[146,6783,6784],{"class":148,"line":1658},[146,6785,1082],{"emptyLinePlaceholder":936},[146,6787,6788],{"class":148,"line":1675},[146,6789,1082],{"emptyLinePlaceholder":936},[146,6791,6792],{"class":148,"line":1686},[146,6793,1082],{"emptyLinePlaceholder":936},[146,6795,6796],{"class":148,"line":1696},[146,6797,3803],{},[146,6799,6800],{"class":148,"line":1713},[146,6801,3808],{},[146,6803,6804],{"class":148,"line":1724},[146,6805,3813],{},[146,6807,6808],{"class":148,"line":1730},[146,6809,1082],{"emptyLinePlaceholder":936},[146,6811,6812],{"class":148,"line":1742},[146,6813,3822],{},[146,6815,6816],{"class":148,"line":1747},[146,6817,3827],{},[146,6819,6820],{"class":148,"line":2041},[146,6821,3832],{},[146,6823,6824],{"class":148,"line":2047},[146,6825,1082],{"emptyLinePlaceholder":936},[146,6827,6828],{"class":148,"line":4},[146,6829,3841],{},[146,6831,6832],{"class":148,"line":2058},[146,6833,1082],{"emptyLinePlaceholder":936},[146,6835,6836],{"class":148,"line":2063},[146,6837,3850],{},[146,6839,6840],{"class":148,"line":2081},[146,6841,3855],{},[146,6843,6844],{"class":148,"line":2098},[146,6845,1082],{"emptyLinePlaceholder":936},[146,6847,6848],{"class":148,"line":2120},[146,6849,3864],{},[146,6851,6852],{"class":148,"line":2133},[146,6853,1082],{"emptyLinePlaceholder":936},[146,6855,6856],{"class":148,"line":2140},[146,6857,3873],{},[146,6859,6860],{"class":148,"line":2145},[146,6861,1082],{"emptyLinePlaceholder":936},[146,6863,6864],{"class":148,"line":2157},[146,6865,3882],{},[146,6867,6868],{"class":148,"line":2163},[146,6869,347],{},[13,6871,6872,6873,6876],{},"このファイルはコンテナ内の",[102,6874,6875],{},"nginx.conf","にボリュームします。tiangolo\u002Fnginx-rtmpのイメージで生成されるnginx.confはRTMPのために最低限の記述しかないので上記のようにします。",[13,6878,6879],{},"RTMPでは以下の設定です。",[137,6881,6883],{"className":3632,"code":6882,"language":3635,"meta":142,"style":142},"rtmp {\n    server {\n        listen 1935;\n        listen [::]:1935 ipv6only=on;\n        access_log \u002Fvar\u002Flog\u002Frtmp_access.log;\n        chunk_size 4096;\n        timeout 10s;\n\n        application live {\n            live on;\n\n            # HLSの記述欄\n            hls on;\n            # ここに映像ファイルが配置される\n            hls_path \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\u002Fhls;\n            hls_fragment 10s;\n        }\n    }\n}\n",[102,6884,6885,6889,6893,6897,6901,6905,6909,6913,6917,6921,6925,6929,6933,6937,6941,6945,6949,6953,6957],{"__ignoreMap":142},[146,6886,6887],{"class":148,"line":149},[146,6888,3696],{},[146,6890,6891],{"class":148,"line":156},[146,6892,3701],{},[146,6894,6895],{"class":148,"line":184},[146,6896,3706],{},[146,6898,6899],{"class":148,"line":199},[146,6900,3711],{},[146,6902,6903],{"class":148,"line":205},[146,6904,3716],{},[146,6906,6907],{"class":148,"line":228},[146,6908,3721],{},[146,6910,6911],{"class":148,"line":249},[146,6912,3726],{},[146,6914,6915],{"class":148,"line":270},[146,6916,1082],{"emptyLinePlaceholder":936},[146,6918,6919],{"class":148,"line":284},[146,6920,3735],{},[146,6922,6923],{"class":148,"line":296},[146,6924,3740],{},[146,6926,6927],{"class":148,"line":302},[146,6928,1082],{"emptyLinePlaceholder":936},[146,6930,6931],{"class":148,"line":316},[146,6932,3749],{},[146,6934,6935],{"class":148,"line":326},[146,6936,3754],{},[146,6938,6939],{"class":148,"line":332},[146,6940,3759],{},[146,6942,6943],{"class":148,"line":338},[146,6944,6765],{},[146,6946,6947],{"class":148,"line":344},[146,6948,3769],{},[146,6950,6951],{"class":148,"line":1396},[146,6952,335],{},[146,6954,6955],{"class":148,"line":1420},[146,6956,1633],{},[146,6958,6959],{"class":148,"line":1425},[146,6960,347],{},[13,6962,6963,6964,6966],{},"ここでは1935ポートでRTMPをリッスンし、また",[102,6965,6485],{},"というURLでアップロードできるようにしています。",[819,6968,6969],{"id":6969},"web",[13,6971,6972,6973,6976],{},"webは以下のようにしておきます。ドキュメントルートは",[102,6974,6975],{},"\u002Fusr\u002Fshare\u002Fnginx\u002Fhtml","です。",[137,6978,6980],{"className":3632,"code":6979,"filename":3634,"language":3635,"meta":142,"style":142},"server {\n    listen       80;\n    listen  [::]:80;\n    server_name  localhost;\n\n    #access_log  \u002Fvar\u002Flog\u002Fnginx\u002Fhost.access.log  main;\n\n    location \u002F {\n        root   \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml;\n        index  index.html index.htm;\n    }\n\n    #error_page  404              \u002F404.html;\n\n    # redirect server error pages to the static page \u002F50x.html\n    #\n    error_page   500 502 503 504  \u002F50x.html;\n    location = \u002F50x.html {\n        root   \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml;\n    }\n\n    # proxy the PHP scripts to Apache listening on 127.0.0.1:80\n    #\n    #location ~ \\.php$ {\n    #    proxy_pass   http:\u002F\u002F127.0.0.1;\n    #}\n\n    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n    #\n    #location ~ \\.php$ {\n    #    root           html;\n    #    fastcgi_pass   127.0.0.1:9000;\n    #    fastcgi_index  index.php;\n    #    fastcgi_param  SCRIPT_FILENAME  \u002Fscripts$fastcgi_script_name;\n    #    include        fastcgi_params;\n    #}\n\n    # deny access to .htaccess files, if Apache's document root\n    # concurs with nginx's one\n    #\n    #location ~ \u002F\\.ht {\n    #    deny  all;\n    #}\n}\n",[102,6981,6982,6986,6990,6994,6998,7002,7006,7010,7014,7019,7023,7027,7031,7035,7039,7043,7047,7051,7055,7059,7063,7067,7071,7075,7079,7083,7087,7091,7095,7099,7103,7107,7111,7115,7119,7123,7127,7131,7135,7139,7143,7147,7151,7155],{"__ignoreMap":142},[146,6983,6984],{"class":148,"line":149},[146,6985,3899],{},[146,6987,6988],{"class":148,"line":156},[146,6989,3904],{},[146,6991,6992],{"class":148,"line":184},[146,6993,3909],{},[146,6995,6996],{"class":148,"line":199},[146,6997,3914],{},[146,6999,7000],{"class":148,"line":205},[146,7001,1082],{"emptyLinePlaceholder":936},[146,7003,7004],{"class":148,"line":228},[146,7005,3923],{},[146,7007,7008],{"class":148,"line":249},[146,7009,1082],{"emptyLinePlaceholder":936},[146,7011,7012],{"class":148,"line":270},[146,7013,3932],{},[146,7015,7016],{"class":148,"line":284},[146,7017,7018],{},"        root   \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml;\n",[146,7020,7021],{"class":148,"line":296},[146,7022,3942],{},[146,7024,7025],{"class":148,"line":302},[146,7026,1633],{},[146,7028,7029],{"class":148,"line":316},[146,7030,1082],{"emptyLinePlaceholder":936},[146,7032,7033],{"class":148,"line":326},[146,7034,3955],{},[146,7036,7037],{"class":148,"line":332},[146,7038,1082],{"emptyLinePlaceholder":936},[146,7040,7041],{"class":148,"line":338},[146,7042,3964],{},[146,7044,7045],{"class":148,"line":344},[146,7046,3969],{},[146,7048,7049],{"class":148,"line":1396},[146,7050,3974],{},[146,7052,7053],{"class":148,"line":1420},[146,7054,3979],{},[146,7056,7057],{"class":148,"line":1425},[146,7058,7018],{},[146,7060,7061],{"class":148,"line":1431},[146,7062,1633],{},[146,7064,7065],{"class":148,"line":1488},[146,7066,1082],{"emptyLinePlaceholder":936},[146,7068,7069],{"class":148,"line":1518},[146,7070,3996],{},[146,7072,7073],{"class":148,"line":1527},[146,7074,3969],{},[146,7076,7077],{"class":148,"line":1533},[146,7078,4005],{},[146,7080,7081],{"class":148,"line":1566},[146,7082,4010],{},[146,7084,7085],{"class":148,"line":1592},[146,7086,4015],{},[146,7088,7089],{"class":148,"line":1620},[146,7090,1082],{"emptyLinePlaceholder":936},[146,7092,7093],{"class":148,"line":1630},[146,7094,4024],{},[146,7096,7097],{"class":148,"line":1636},[146,7098,3969],{},[146,7100,7101],{"class":148,"line":1641},[146,7102,4005],{},[146,7104,7105],{"class":148,"line":1646},[146,7106,4037],{},[146,7108,7109],{"class":148,"line":1658},[146,7110,4042],{},[146,7112,7113],{"class":148,"line":1675},[146,7114,4047],{},[146,7116,7117],{"class":148,"line":1686},[146,7118,4052],{},[146,7120,7121],{"class":148,"line":1696},[146,7122,4057],{},[146,7124,7125],{"class":148,"line":1713},[146,7126,4015],{},[146,7128,7129],{"class":148,"line":1724},[146,7130,1082],{"emptyLinePlaceholder":936},[146,7132,7133],{"class":148,"line":1730},[146,7134,4070],{},[146,7136,7137],{"class":148,"line":1742},[146,7138,4075],{},[146,7140,7141],{"class":148,"line":1747},[146,7142,3969],{},[146,7144,7145],{"class":148,"line":2041},[146,7146,4084],{},[146,7148,7149],{"class":148,"line":2047},[146,7150,4089],{},[146,7152,7153],{"class":148,"line":4},[146,7154,4015],{},[146,7156,7157],{"class":148,"line":2058},[146,7158,347],{},[13,7160,7161],{},"RTMPを含むNginxの設定はこれでOKです。",[419,7163,7164],{"id":7164},"web側実装",[13,7166,7167,7170],{},[102,7168,7169],{},"html\u002Ftest.html","として以下のように作成します。",[137,7172,7175],{"className":2651,"code":7173,"filename":7174,"language":2653,"meta":142,"style":142},"\u003C!DOCTYPE html>\n\u003Chtml>\n\n\u003Chead>\n  \u003Cmeta charset=\"utf-8\">\n  \u003Ctitle>MediaElement\u003C\u002Ftitle>\n  \u003C!-- MediaElement style -->\n  \u003Clink rel=\"stylesheet\" href=\"\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fmediaelement\u002F4.2.9\u002Fmediaelementplayer.css\" \u002F>\n\u003C\u002Fhead>\n\n\u003Cbody>\n  \u003C!-- MediaElement -->\n  \u003Cscript src=\"\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fmediaelement\u002F4.2.9\u002Fmediaelement-and-player.js\">\u003C\u002Fscript>\n\n  \u003Cvideo id=\"player\" width=\"640\" height=\"360\">\n\u003C\u002Fbody>\n\u003Cscript type=\"text\u002Fjavascript\">\n\n      var player = new MediaElementPlayer('player', {\n        success: function(mediaElement, originalNode) {\n          console.log(\"Player initialised\");\n        }\n      });\n        player.setSrc(\"hls\u002F.m3u8\");\n\u003C\u002Fscript>\n\n\u003C\u002Fhtml>\n","test.html",[102,7176,7177,7187,7195,7199,7207,7225,7241,7245,7273,7281,7285,7293,7297,7319,7323,7361,7369,7387,7391,7415,7435,7455,7459,7467,7488,7496,7500],{"__ignoreMap":142},[146,7178,7179,7181,7183,7185],{"class":148,"line":149},[146,7180,2660],{"class":152},[146,7182,2663],{"class":1214},[146,7184,2666],{"class":162},[146,7186,2669],{"class":152},[146,7188,7189,7191,7193],{"class":148,"line":156},[146,7190,1170],{"class":152},[146,7192,2653],{"class":1214},[146,7194,2669],{"class":152},[146,7196,7197],{"class":148,"line":184},[146,7198,1082],{"emptyLinePlaceholder":936},[146,7200,7201,7203,7205],{"class":148,"line":199},[146,7202,1170],{"class":152},[146,7204,2684],{"class":1214},[146,7206,2669],{"class":152},[146,7208,7209,7211,7213,7215,7217,7219,7221,7223],{"class":148,"line":205},[146,7210,4295],{"class":152},[146,7212,4298],{"class":1214},[146,7214,4301],{"class":162},[146,7216,1093],{"class":152},[146,7218,166],{"class":152},[146,7220,4308],{"class":175},[146,7222,166],{"class":152},[146,7224,2669],{"class":152},[146,7226,7227,7229,7231,7233,7235,7237,7239],{"class":148,"line":228},[146,7228,4295],{"class":152},[146,7230,2694],{"class":1214},[146,7232,2697],{"class":152},[146,7234,4323],{"class":866},[146,7236,2703],{"class":152},[146,7238,2694],{"class":1214},[146,7240,2669],{"class":152},[146,7242,7243],{"class":148,"line":249},[146,7244,4334],{"class":1141},[146,7246,7247,7249,7251,7253,7255,7257,7259,7261,7263,7265,7267,7269,7271],{"class":148,"line":270},[146,7248,4295],{"class":152},[146,7250,2714],{"class":1214},[146,7252,2717],{"class":162},[146,7254,1093],{"class":152},[146,7256,166],{"class":152},[146,7258,2724],{"class":175},[146,7260,166],{"class":152},[146,7262,2729],{"class":162},[146,7264,1093],{"class":152},[146,7266,166],{"class":152},[146,7268,4359],{"class":175},[146,7270,166],{"class":152},[146,7272,2741],{"class":152},[146,7274,7275,7277,7279],{"class":148,"line":284},[146,7276,2703],{"class":152},[146,7278,2684],{"class":1214},[146,7280,2669],{"class":152},[146,7282,7283],{"class":148,"line":296},[146,7284,1082],{"emptyLinePlaceholder":936},[146,7286,7287,7289,7291],{"class":148,"line":302},[146,7288,1170],{"class":152},[146,7290,2782],{"class":1214},[146,7292,2669],{"class":152},[146,7294,7295],{"class":148,"line":316},[146,7296,4388],{"class":1141},[146,7298,7299,7301,7303,7305,7307,7309,7311,7313,7315,7317],{"class":148,"line":326},[146,7300,4295],{"class":152},[146,7302,2748],{"class":1214},[146,7304,2751],{"class":162},[146,7306,1093],{"class":152},[146,7308,166],{"class":152},[146,7310,4403],{"class":175},[146,7312,166],{"class":152},[146,7314,2763],{"class":152},[146,7316,2748],{"class":1214},[146,7318,2669],{"class":152},[146,7320,7321],{"class":148,"line":332},[146,7322,1082],{"emptyLinePlaceholder":936},[146,7324,7325,7327,7329,7331,7333,7335,7337,7339,7341,7343,7345,7347,7349,7351,7353,7355,7357,7359],{"class":148,"line":338},[146,7326,4295],{"class":152},[146,7328,4422],{"class":1214},[146,7330,2793],{"class":162},[146,7332,1093],{"class":152},[146,7334,166],{"class":152},[146,7336,4431],{"class":175},[146,7338,166],{"class":152},[146,7340,4436],{"class":162},[146,7342,1093],{"class":152},[146,7344,166],{"class":152},[146,7346,4443],{"class":175},[146,7348,166],{"class":152},[146,7350,4448],{"class":162},[146,7352,1093],{"class":152},[146,7354,166],{"class":152},[146,7356,4455],{"class":175},[146,7358,166],{"class":152},[146,7360,2669],{"class":152},[146,7362,7363,7365,7367],{"class":148,"line":344},[146,7364,2703],{"class":152},[146,7366,2782],{"class":1214},[146,7368,2669],{"class":152},[146,7370,7371,7373,7375,7377,7379,7381,7383,7385],{"class":148,"line":1396},[146,7372,1170],{"class":152},[146,7374,2748],{"class":1214},[146,7376,4476],{"class":162},[146,7378,1093],{"class":152},[146,7380,166],{"class":152},[146,7382,4483],{"class":175},[146,7384,166],{"class":152},[146,7386,2669],{"class":152},[146,7388,7389],{"class":148,"line":1420},[146,7390,1082],{"emptyLinePlaceholder":936},[146,7392,7393,7395,7397,7399,7401,7403,7405,7407,7409,7411,7413],{"class":148,"line":1425},[146,7394,4496],{"class":162},[146,7396,4499],{"class":866},[146,7398,1093],{"class":152},[146,7400,4504],{"class":152},[146,7402,4507],{"class":1268},[146,7404,1360],{"class":866},[146,7406,1049],{"class":152},[146,7408,4431],{"class":175},[146,7410,1049],{"class":152},[146,7412,1099],{"class":152},[146,7414,861],{"class":152},[146,7416,7417,7419,7421,7423,7425,7427,7429,7431,7433],{"class":148,"line":1431},[146,7418,4524],{"class":1268},[146,7420,169],{"class":152},[146,7422,4529],{"class":162},[146,7424,1360],{"class":152},[146,7426,4534],{"class":1581},[146,7428,1099],{"class":152},[146,7430,4539],{"class":1581},[146,7432,1384],{"class":152},[146,7434,861],{"class":152},[146,7436,7437,7439,7441,7443,7445,7447,7449,7451,7453],{"class":148,"line":1488},[146,7438,4548],{"class":866},[146,7440,1176],{"class":152},[146,7442,1880],{"class":1268},[146,7444,1360],{"class":1214},[146,7446,166],{"class":152},[146,7448,4559],{"class":175},[146,7450,166],{"class":152},[146,7452,1384],{"class":1214},[146,7454,1052],{"class":152},[146,7456,7457],{"class":148,"line":1518},[146,7458,335],{"class":152},[146,7460,7461,7463,7465],{"class":148,"line":1527},[146,7462,4574],{"class":152},[146,7464,1384],{"class":866},[146,7466,1052],{"class":152},[146,7468,7469,7471,7473,7475,7477,7479,7482,7484,7486],{"class":148,"line":1533},[146,7470,4583],{"class":866},[146,7472,1176],{"class":152},[146,7474,4588],{"class":1268},[146,7476,1360],{"class":866},[146,7478,166],{"class":152},[146,7480,7481],{"class":175},"hls\u002F.m3u8",[146,7483,166],{"class":152},[146,7485,1384],{"class":866},[146,7487,1052],{"class":152},[146,7489,7490,7492,7494],{"class":148,"line":1566},[146,7491,2703],{"class":152},[146,7493,2748],{"class":1214},[146,7495,2669],{"class":152},[146,7497,7498],{"class":148,"line":1592},[146,7499,1082],{"emptyLinePlaceholder":936},[146,7501,7502,7504,7506],{"class":148,"line":1620},[146,7503,2703],{"class":152},[146,7505,2653],{"class":1214},[146,7507,2669],{"class":152},[13,7509,7510,7511,7513],{},"hlsが再生できるようにjsの再生ライブラリを用意します。",[102,7512,7481],{},"については後述します。",[419,7515,7516],{"id":7516},"dokcer起動",[137,7518,7521],{"className":7519,"code":7520,"language":3289},[3287],"docker-compose up -d\n",[102,7522,7520],{"__ignoreMap":142},[13,7524,7525],{},"を用いてdockerイメージを起動しましょう。",[46,7527,7529],{"id":7528},"配信テスト","配信テスト！",[419,7531,7533],{"id":7532},"macのipを確認","MacのIPを確認",[13,7535,7536],{},"今回のはMac上にRTMPサーバを立てたのでMacのIPを確認しておきます。「システム環境設定」から「ネットワーク」を選択し、Wi-FiからIPアドレスを確認します。このPCは「192.168.0.3」なので",[13,7538,7539,7540,7543,7544,7547],{},"RTMPは",[102,7541,7542],{},"rtmp:\u002F\u002F192.168.0.3\u002Flive","、webは",[102,7545,7546],{},"http:\u002F\u002F192.168.0.3\u002Findex.html","でアクセスできます。",[74,7549],{":src":7550,":width":77},"'rtmp-docker-local\u002Fmac.png'",[419,7552,7553],{"id":7553},"gopro側の設定",[13,7555,7556],{},"Goproとアプリのテザリングなどは省略します。",[13,7558,7559,7564],{},[53,7560,7563],{"href":7561,"rel":7562},"https:\u002F\u002Fapps.apple.com\u002Fjp\u002Fapp\u002Fgopro-quik-%E5%8B%95%E7%94%BB-%E5%86%99%E7%9C%9F%E7%B7%A8%E9%9B%86%E3%82%A2%E3%83%97%E3%83%AA\u002Fid561350520",[57],"公式アプリ（app store）",[53,7565,7568],{"href":7566,"rel":7567},"https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.gopro.smarty&hl=ja&gl=US",[57],"公式アプリ（android）",[13,7570,7571,7572],{},"goproとアプリを接続して、「ライブの設定」に移動\n",[74,7573],{":src":7574,":width":965,":center":4909},"'rtmp-docker-local\u002Fg1.jpg'",[13,7576,7577,7578],{},"その他、RTMPを選択\n",[74,7579],{":src":7580,":width":965,":center":4909},"'rtmp-docker-local\u002Fg2.jpg'",[13,7582,7583,7584,7586,7587],{},"Macと同じwi-fiに接続して",[102,7585,7542],{},"をURLに入力\n",[74,7588],{":src":7589,":width":965,":center":4909},"'rtmp-docker-local\u002Fg3.jpg'",[13,7591,7592],{},"設定が完了したらライブ配信を開始します。",[419,7594,7595],{"id":7595},"サーバーでは",[13,7597,7598],{},"ライブ配信を開始するとRTMPのURLへ動画がアップされていきます。サーバーの方ではドキュメントルートにこの映像のHLSが配置されるようにしたので、マウントの関係上以下のようなディレクトリが発生していると思います。",[74,7600],{":src":7601,":width":7602,":center":4909},"'rtmp-docker-local\u002Fserver.png'","'100px'",[13,7604,7605,7606,7608],{},"先程のHTMLにあった「",[102,7607,7481],{},"」はサーバーにアップロードされた映像の元でもあるファイルを示しています。",[419,7610,7612],{"id":7611},"ブラウザを見てみると","ブラウザを見てみると..",[13,7614,7615,7616,7619],{},"では",[102,7617,7618],{},"http:\u002F\u002Flocalhost:8085\u002Findex.html","でPCから映像を見てみましょう。再生ボタンを押すと以下のようになりました。",[74,7621],{":src":7622,":width":77},"'rtmp-docker-local\u002Fweb.png'",[13,7624,7625],{},"しっかりとGoproが写している映像が表示され、音声も流れました。画面を写していましたが、いろいろカメラを回すと確かに映像・音声も切り替わります。",[13,7627,7628],{},"とりあえずこれでライブ配信映像を自前で実装したサーバに送ることが確認できました。今度はサーバー内での映像をyoutubeのlivestreaming apiなどを通じて実際にライブ映像に流せるようにしてみます。",[46,7630,3163],{"id":3163},[13,7632,7633],{},"今回参考にした資料です。非常に助かりました。",[17,7635,7636,7643,7650],{},[20,7637,7638],{},[53,7639,7642],{"href":7640,"rel":7641},"https:\u002F\u002Fqiita.com\u002Fkobakazu0429\u002Fitems\u002F33739b83000583c5448e",[57],"docker-nginx-rtmpとiPhoneでお手軽マルチカメラライブストリーミング配信環境の構築",[20,7644,7645],{},[53,7646,7649],{"href":7647,"rel":7648},"https:\u002F\u002Fblog.litus.co.jp\u002F2020\u002F11\u002Fdockerhlswebcentos-nginxffmpegdocker.html",[57],"Dockerコンテナ化させたHLSのwebストリーミング配信環境構築",[20,7651,7652],{},[53,7653,7655],{"href":6536,"rel":7654},[57],"Docker image",[908,7657,5169],{},{"title":142,"searchDepth":184,"depth":184,"links":7659},[7660,7665,7669,7678,7684],{"id":6382,"depth":156,"text":6382,"children":7661},[7662,7663,7664],{"id":6399,"depth":184,"text":6399},{"id":6393,"depth":184,"text":6393},{"id":6428,"depth":184,"text":6428},{"id":6440,"depth":156,"text":6440,"children":7666},[7667,7668],{"id":6471,"depth":184,"text":6472},{"id":6478,"depth":184,"text":6479},{"id":6489,"depth":156,"text":6490,"children":7670},[7671,7672,7676,7677],{"id":6493,"depth":184,"text":6493},{"id":6642,"depth":184,"text":6642,"children":7673},[7674,7675],{"id":3349,"depth":199,"text":6648},{"id":6969,"depth":199,"text":6969},{"id":7164,"depth":184,"text":7164},{"id":7516,"depth":184,"text":7516},{"id":7528,"depth":156,"text":7529,"children":7679},[7680,7681,7682,7683],{"id":7532,"depth":184,"text":7533},{"id":7553,"depth":184,"text":7553},{"id":7595,"depth":184,"text":7595},{"id":7611,"depth":184,"text":7612},{"id":3163,"depth":156,"text":3163},[930],"2022-05-20","ローカルのDockerとNginxを用いてGoproからライブ配信映像をRTMPを再生する",{},"\u002Fseries\u002Frtmp-manager-server-1",{"title":6367,"description":7687},"series\u002Frtmp-manager-server-1",[5201,5202],"rtmp-docker-local\u002Fthumbnail.png","_e2Aj1JaZSf-KJSGe6AqjuPlf5YdLmKDjuCXzhFw0dg",{"id":7696,"title":7697,"body":7698,"category":8732,"createdAt":8733,"description":8734,"extension":933,"index":934,"meta":8735,"navigation":936,"path":8736,"publish":936,"seo":8737,"series":934,"seriesTitle":934,"stem":8738,"tag":8739,"thumbnail":8741,"updatedAt":934,"__hash__":8742},"articles\u002Farticles\u002Fnuxt-auth-middleware.md","Nuxt.jsのSSR・SPA時のフロント側の認証ビューを自前で実装する",{"type":10,"value":7699,"toc":8720},[7700,7703,7706,7709,7720,7723,7726,7729,7732,7735,7738,7741,7752,7756,7759,7802,7805,7866,7869,7876,7956,7959,7962,8067,8074,8107,8111,8116,8123,8219,8326,8330,8337,8341,8350,8353,8523,8526,8530,8537,8697,8700,8703,8706,8717],[13,7701,7702],{},"こんにちはjunです。webアプリを作成する時は大体、認証機能をつけることが多いです。LaravelやDjangoなどでは一発で行けますが、Nuxt.js、Vue.jsを使用したSPA、Node.jsでのSSRコンテンツを作成する場合は一捻り必要です。",[13,7704,7705],{},"Nuxt.jsなどで作成するアプリはバックエンドと独立し、都度バックエンドへのAPI通信の際にトークンを渡したりすることで認証が必要なAPIへアクセスしています。",[13,7707,7708],{},"ビュー側の処理でも",[17,7710,7711,7714,7717],{},[20,7712,7713],{},"ログインしているユーザーはこのように表示",[20,7715,7716],{},"このルートはログインしているユーザーのみアクセス可能",[20,7718,7719],{},"未ログインユーザーはログイン画面移動",[13,7721,7722],{},"といった処理が行われることが多いです。この記事では上記のような認証を用いたビューの表現、ルーターの設定をNuxt.jsでどう行うかの解説をしたいと思います。SPA（クライアントサイドレンダリング）とSSR（サーバーサイドレンダリング）の２パターンを実装します。ちなみにnuxt-authなどのライブラリは使用しません。",[13,7724,7725],{},"また、ログインメソッドの処理やリクエストヘッダにどうこうするとか、バックエンド側の処理については今回は解説しません。あくまでフロント側（Nuxt.js側）の表示やロジックに認証が必要な場合にどう実装するかについて解説するのみです。",[46,7727,7728],{"id":7728},"大まかな処理",[13,7730,7731],{},"まずこの実装を行うにあたりJWTやクッキーなどなんらかの認証トークンが取得できていること、またそれらの値を使用できることを前提として進めます。",[13,7733,7734],{},"LaravelやDjangoなどではリクエストを送る際にヘッダーにセッションIDを含むクッキーを送信することで、サーバー側でログインによるビューやロジックの分岐を行っています。",[13,7736,7737],{},"しかしNuxt.jsでSPAの場合はコンテンツをクライアント側で生成し、またSSRの場合はnode.jsのサーバーで行われます。すなわちセッションやユーザー情報を保存しているAPIサーバーから「どうにかしてNuxt.js側にユーザー情報を渡す」必要があります。",[13,7739,7740],{},"これから実装する内容はSSR、SPAどちらも以下の通りです。",[398,7742,7743,7746,7749],{},[20,7744,7745],{},"Nuxt.js側でユーザーの情報を保存する。",[20,7747,7748],{},"ログインの是非はユーザー情報の有無で判断する。",[20,7750,7751],{},"何かしらのタイミングでユーザー情報を取得するAPIを都度発行する。",[46,7753,7755],{"id":7754},"storeの調整","Storeの調整",[13,7757,7758],{},"ではまずStoreにて以下のようにuserステートを作成します。",[137,7760,7763],{"className":3069,"code":7761,"filename":7762,"language":3071,"meta":142,"style":142},"export const state = () => ({\n    user:null,\n});\n","store\u002Findex.js",[102,7764,7765,7786,7794],{"__ignoreMap":142},[146,7766,7767,7770,7773,7776,7778,7780,7782,7784],{"class":148,"line":149},[146,7768,7769],{"class":1027},"export",[146,7771,7772],{"class":162}," const",[146,7774,7775],{"class":866}," state ",[146,7777,1093],{"class":152},[146,7779,5846],{"class":152},[146,7781,1587],{"class":162},[146,7783,1150],{"class":866},[146,7785,153],{"class":152},[146,7787,7788,7791],{"class":148,"line":156},[146,7789,7790],{"class":1214},"    user",[146,7792,7793],{"class":152},":null,\n",[146,7795,7796,7798,7800],{"class":148,"line":184},[146,7797,1733],{"class":152},[146,7799,1384],{"class":866},[146,7801,1052],{"class":152},[13,7803,7804],{},"デフォルトではnullにしておきます。このuserステートがnullでなく、ユーザー情報のオブジェクトである場合をログイン状態とします。mutationなどでこのステートに値をセットできるように作っておきます。",[137,7806,7808],{"className":3069,"code":7807,"filename":7762,"language":3071,"meta":142,"style":142},"export const mutations = {\n    setUser(state,{user}){\n            state.user = user;\n    }\n}\n",[102,7809,7810,7823,7842,7858,7862],{"__ignoreMap":142},[146,7811,7812,7814,7816,7819,7821],{"class":148,"line":149},[146,7813,7769],{"class":1027},[146,7815,7772],{"class":162},[146,7817,7818],{"class":866}," mutations ",[146,7820,1093],{"class":152},[146,7822,861],{"class":152},[146,7824,7825,7828,7830,7833,7836,7839],{"class":148,"line":156},[146,7826,7827],{"class":1214},"    setUser",[146,7829,1360],{"class":152},[146,7831,7832],{"class":1581},"state",[146,7834,7835],{"class":152},",{",[146,7837,7838],{"class":1581},"user",[146,7840,7841],{"class":152},"}){\n",[146,7843,7844,7847,7849,7851,7853,7856],{"class":148,"line":184},[146,7845,7846],{"class":866},"            state",[146,7848,1176],{"class":152},[146,7850,7838],{"class":866},[146,7852,1209],{"class":152},[146,7854,7855],{"class":866}," user",[146,7857,1052],{"class":152},[146,7859,7860],{"class":148,"line":199},[146,7861,1633],{"class":152},[146,7863,7864],{"class":148,"line":205},[146,7865,347],{"class":152},[46,7867,7868],{"id":7868},"ミドルェアの作成",[13,7870,7871,7872,7875],{},"次に「ログインしたユーザーのみがアクセス可能なページ」を実装できるようにミドルウェアを実装します。",[102,7873,7874],{},"middleware\u002Fauth.js","を作成します。",[137,7877,7879],{"className":3069,"code":7878,"filename":7874,"language":3071,"meta":142,"style":142},"export default function ({ store, redirect }) {\n    if (!store.state.user) {\n        redirect('\u002Flogin');\n    }\n}\n",[102,7880,7881,7906,7930,7948,7952],{"__ignoreMap":142},[146,7882,7883,7885,7888,7890,7893,7896,7898,7901,7904],{"class":148,"line":149},[146,7884,7769],{"class":1027},[146,7886,7887],{"class":1027}," default",[146,7889,4529],{"class":162},[146,7891,7892],{"class":152}," ({",[146,7894,7895],{"class":1581}," store",[146,7897,1099],{"class":152},[146,7899,7900],{"class":1581}," redirect",[146,7902,7903],{"class":152}," })",[146,7905,861],{"class":152},[146,7907,7908,7910,7912,7915,7918,7920,7922,7924,7926,7928],{"class":148,"line":156},[146,7909,1434],{"class":1027},[146,7911,1150],{"class":1214},[146,7913,7914],{"class":152},"!",[146,7916,7917],{"class":866},"store",[146,7919,1176],{"class":152},[146,7921,7832],{"class":866},[146,7923,1176],{"class":152},[146,7925,7838],{"class":866},[146,7927,1196],{"class":1214},[146,7929,153],{"class":152},[146,7931,7932,7935,7937,7939,7942,7944,7946],{"class":148,"line":184},[146,7933,7934],{"class":1268},"        redirect",[146,7936,1360],{"class":1214},[146,7938,1049],{"class":152},[146,7940,7941],{"class":175},"\u002Flogin",[146,7943,1049],{"class":152},[146,7945,1384],{"class":1214},[146,7947,1052],{"class":152},[146,7949,7950],{"class":148,"line":199},[146,7951,1633],{"class":152},[146,7953,7954],{"class":148,"line":205},[146,7955,347],{"class":152},[13,7957,7958],{},"単純にStoreのUserステートがnullかどうかでログインページに飛ばすようにしています。",[13,7960,7961],{},"ページコンポーネントでは以下のようにしてミドルウェアを有効にします。",[137,7963,7968],{"className":7964,"code":7965,"filename":7966,"language":7967,"meta":142,"style":142},"language-vue shiki shiki-themes material-theme-ocean","\u003Ctemplate>\n    \u003Cdiv>\n        auth\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\u003Cscript>\nexport default {\n    name:\"home\",\n    middleware:\"auth\",\n}\n\u003C\u002Fscript>\n","pages\u002Fauth\u002Findex.vue","vue",[102,7969,7970,7979,7987,7992,8000,8008,8016,8024,8040,8055,8059],{"__ignoreMap":142},[146,7971,7972,7974,7977],{"class":148,"line":149},[146,7973,1170],{"class":152},[146,7975,7976],{"class":1214},"template",[146,7978,2669],{"class":152},[146,7980,7981,7983,7985],{"class":148,"line":156},[146,7982,2691],{"class":152},[146,7984,39],{"class":1214},[146,7986,2669],{"class":152},[146,7988,7989],{"class":148,"line":184},[146,7990,7991],{"class":866},"        auth\n",[146,7993,7994,7996,7998],{"class":148,"line":199},[146,7995,3000],{"class":152},[146,7997,39],{"class":1214},[146,7999,2669],{"class":152},[146,8001,8002,8004,8006],{"class":148,"line":205},[146,8003,2703],{"class":152},[146,8005,7976],{"class":1214},[146,8007,2669],{"class":152},[146,8009,8010,8012,8014],{"class":148,"line":228},[146,8011,1170],{"class":152},[146,8013,2748],{"class":1214},[146,8015,2669],{"class":152},[146,8017,8018,8020,8022],{"class":148,"line":249},[146,8019,7769],{"class":1027},[146,8021,7887],{"class":1027},[146,8023,861],{"class":152},[146,8025,8026,8029,8031,8033,8036,8038],{"class":148,"line":270},[146,8027,8028],{"class":1214},"    name",[146,8030,169],{"class":152},[146,8032,166],{"class":152},[146,8034,8035],{"class":175},"home",[146,8037,166],{"class":152},[146,8039,181],{"class":152},[146,8041,8042,8045,8047,8049,8051,8053],{"class":148,"line":284},[146,8043,8044],{"class":1214},"    middleware",[146,8046,169],{"class":152},[146,8048,166],{"class":152},[146,8050,5580],{"class":175},[146,8052,166],{"class":152},[146,8054,181],{"class":152},[146,8056,8057],{"class":148,"line":296},[146,8058,347],{"class":152},[146,8060,8061,8063,8065],{"class":148,"line":302},[146,8062,2703],{"class":152},[146,8064,2748],{"class":1214},[146,8066,2669],{"class":152},[13,8068,8069,8070,8073],{},"こうすることでログインが必要なページを実装することができます。または",[102,8071,8072],{},"nuxt.config.js"," にて以下のように設定することで全てのページに認証ミドルウェアを適用できます。",[137,8075,8077],{"className":3069,"code":8076,"filename":8072,"language":3071,"meta":142,"style":142},"router: {\n    middleware: 'auth',\n},\n",[102,8078,8079,8088,8102],{"__ignoreMap":142},[146,8080,8081,8084,8086],{"class":148,"line":149},[146,8082,8083],{"class":211},"router",[146,8085,169],{"class":152},[146,8087,861],{"class":152},[146,8089,8090,8092,8094,8096,8098,8100],{"class":148,"line":156},[146,8091,8044],{"class":211},[146,8093,169],{"class":152},[146,8095,1043],{"class":152},[146,8097,5580],{"class":175},[146,8099,1049],{"class":152},[146,8101,181],{"class":152},[146,8103,8104],{"class":148,"line":184},[146,8105,8106],{"class":152},"},\n",[419,8108,8110],{"id":8109},"特定のページディレクトリを除く場合","特定のページ、ディレクトリを除く場合",[13,8112,8113,8115],{},[102,8114,8072],{}," にてグローバルな認証ミドルウェアを実装できますが、未ログインでもアクセス可能なページや、未ログインでないと閲覧できないページ（ログインページなど）では不便です。",[13,8117,8118,8119,8122],{},"個別にミドルウェアを作成してページごとに設定してオーバーライドすることも可能ですが、私はよく以下のように",[102,8120,8121],{},"auth.js","実装しています。",[137,8124,8126],{"className":3069,"code":8125,"filename":7874,"language":3071,"meta":142,"style":142},"\u002F\u002F 特定のページの認証を外す\nexport default function ({ store, redirect }) {\n    if (!store.state.user && route.fullPath !== '\u002Flogin') {\n        redirect('\u002Flogin');\n    }\n}\n",[102,8127,8128,8133,8153,8195,8211,8215],{"__ignoreMap":142},[146,8129,8130],{"class":148,"line":149},[146,8131,8132],{"class":1141},"\u002F\u002F 特定のページの認証を外す\n",[146,8134,8135,8137,8139,8141,8143,8145,8147,8149,8151],{"class":148,"line":156},[146,8136,7769],{"class":1027},[146,8138,7887],{"class":1027},[146,8140,4529],{"class":162},[146,8142,7892],{"class":152},[146,8144,7895],{"class":1581},[146,8146,1099],{"class":152},[146,8148,7900],{"class":1581},[146,8150,7903],{"class":152},[146,8152,861],{"class":152},[146,8154,8155,8157,8159,8161,8163,8165,8167,8169,8171,8174,8177,8179,8182,8185,8187,8189,8191,8193],{"class":148,"line":184},[146,8156,1434],{"class":1027},[146,8158,1150],{"class":1214},[146,8160,7914],{"class":152},[146,8162,7917],{"class":866},[146,8164,1176],{"class":152},[146,8166,7832],{"class":866},[146,8168,1176],{"class":152},[146,8170,7838],{"class":866},[146,8172,8173],{"class":152}," &&",[146,8175,8176],{"class":866}," route",[146,8178,1176],{"class":152},[146,8180,8181],{"class":866},"fullPath",[146,8183,8184],{"class":152}," !==",[146,8186,1043],{"class":152},[146,8188,7941],{"class":175},[146,8190,1049],{"class":152},[146,8192,1196],{"class":1214},[146,8194,153],{"class":152},[146,8196,8197,8199,8201,8203,8205,8207,8209],{"class":148,"line":199},[146,8198,7934],{"class":1268},[146,8200,1360],{"class":1214},[146,8202,1049],{"class":152},[146,8204,7941],{"class":175},[146,8206,1049],{"class":152},[146,8208,1384],{"class":1214},[146,8210,1052],{"class":152},[146,8212,8213],{"class":148,"line":205},[146,8214,1633],{"class":152},[146,8216,8217],{"class":148,"line":228},[146,8218,347],{"class":152},[137,8220,8222],{"className":3069,"code":8221,"filename":7874,"language":3071,"meta":142,"style":142},"\u002F\u002F 特定のディレクトリ配下を外す\nexport default function ({ store, redirect }) {\n    if (!store.state.user && route.fullPath.indexOf('\u002Fpublic') != -1) {\n        redirect('\u002Flogin');\n    }\n}\n",[102,8223,8224,8229,8249,8302,8318,8322],{"__ignoreMap":142},[146,8225,8226],{"class":148,"line":149},[146,8227,8228],{"class":1141},"\u002F\u002F 特定のディレクトリ配下を外す\n",[146,8230,8231,8233,8235,8237,8239,8241,8243,8245,8247],{"class":148,"line":156},[146,8232,7769],{"class":1027},[146,8234,7887],{"class":1027},[146,8236,4529],{"class":162},[146,8238,7892],{"class":152},[146,8240,7895],{"class":1581},[146,8242,1099],{"class":152},[146,8244,7900],{"class":1581},[146,8246,7903],{"class":152},[146,8248,861],{"class":152},[146,8250,8251,8253,8255,8257,8259,8261,8263,8265,8267,8269,8271,8273,8275,8277,8280,8282,8284,8287,8289,8291,8294,8296,8298,8300],{"class":148,"line":184},[146,8252,1434],{"class":1027},[146,8254,1150],{"class":1214},[146,8256,7914],{"class":152},[146,8258,7917],{"class":866},[146,8260,1176],{"class":152},[146,8262,7832],{"class":866},[146,8264,1176],{"class":152},[146,8266,7838],{"class":866},[146,8268,8173],{"class":152},[146,8270,8176],{"class":866},[146,8272,1176],{"class":152},[146,8274,8181],{"class":866},[146,8276,1176],{"class":152},[146,8278,8279],{"class":1268},"indexOf",[146,8281,1360],{"class":1214},[146,8283,1049],{"class":152},[146,8285,8286],{"class":175},"\u002Fpublic",[146,8288,1049],{"class":152},[146,8290,1196],{"class":1214},[146,8292,8293],{"class":152},"!=",[146,8295,2870],{"class":152},[146,8297,1280],{"class":1161},[146,8299,1196],{"class":1214},[146,8301,153],{"class":152},[146,8303,8304,8306,8308,8310,8312,8314,8316],{"class":148,"line":199},[146,8305,7934],{"class":1268},[146,8307,1360],{"class":1214},[146,8309,1049],{"class":152},[146,8311,7941],{"class":175},[146,8313,1049],{"class":152},[146,8315,1384],{"class":1214},[146,8317,1052],{"class":152},[146,8319,8320],{"class":148,"line":205},[146,8321,1633],{"class":152},[146,8323,8324],{"class":148,"line":228},[146,8325,347],{"class":152},[46,8327,8329],{"id":8328},"meリクエストを初期処理にいれる","meリクエストを初期処理にいれる。",[13,8331,8332,8333,8336],{},"storeとミドルェアの準備ができたので、APIサーバーに通信をしてユーザー情報を取得するメソッドを作成しておきます。ここではmeリクエストと呼ぶことにします。今回はaxiosで",[102,8334,8335],{},"https:\u002F\u002Fapi.example.com\u002Fauth\u002Fme","へトークンと一緒にリクエストするとユーザー情報のJSONがレスポンスとして得られるとします。期限切れやトークンがない場合や間違っている場合などは401がレスポンスとして戻ります。",[419,8338,8340],{"id":8339},"spa","SPA",[13,8342,8343,8344,8349],{},"SPAの場合は",[53,8345,8348],{"href":8346,"rel":8347},"https:\u002F\u002Fgithub.com\u002Fpotato4d\u002Fnuxt-client-init-module",[57],"nuxt-client-init-moduleというライブラリ"," を使用するとスムーズです。SSRの場合はNuxtServiceInitという初期処理を実装できるフックがあるのですが、SPAの場合はそれがありあません。Pluginでできなくもないのですが、nuxt-client-init-moduleを使用するとうまくいきやすいです。",[13,8351,8352],{},"上記のライブラリをインストールしてstoreにてmeリクエストをします。",[137,8354,8357],{"className":3069,"code":8355,"filename":8356,"language":3071,"meta":142,"style":142},"export const actions = {\n  async nuxtClientInit({ commit }, context) {\n    await this.$axios.get('https:\u002F\u002Fapi.example.com\u002Fauth\u002Fme')\n    .then(async res=>{\n        commit('setUser', {user:res.data});\n    })\n    .catch(err=>{\n        console.error(err)\n    })\n  }\n}\n","store\u002Findex.js]",[102,8358,8359,8372,8396,8422,8440,8475,8481,8495,8509,8515,8519],{"__ignoreMap":142},[146,8360,8361,8363,8365,8368,8370],{"class":148,"line":149},[146,8362,7769],{"class":1027},[146,8364,7772],{"class":162},[146,8366,8367],{"class":866}," actions ",[146,8369,1093],{"class":152},[146,8371,861],{"class":152},[146,8373,8374,8377,8380,8383,8386,8389,8392,8394],{"class":148,"line":156},[146,8375,8376],{"class":162},"  async",[146,8378,8379],{"class":1214}," nuxtClientInit",[146,8381,8382],{"class":152},"({",[146,8384,8385],{"class":1581}," commit",[146,8387,8388],{"class":152}," },",[146,8390,8391],{"class":1581}," context",[146,8393,1384],{"class":152},[146,8395,861],{"class":152},[146,8397,8398,8401,8404,8407,8409,8412,8414,8416,8418,8420],{"class":148,"line":184},[146,8399,8400],{"class":1027},"    await",[146,8402,8403],{"class":152}," this.",[146,8405,8406],{"class":866},"$axios",[146,8408,1176],{"class":152},[146,8410,8411],{"class":1268},"get",[146,8413,1360],{"class":1214},[146,8415,1049],{"class":152},[146,8417,8335],{"class":175},[146,8419,1049],{"class":152},[146,8421,6104],{"class":1214},[146,8423,8424,8427,8429,8431,8433,8436,8438],{"class":148,"line":199},[146,8425,8426],{"class":152},"    .",[146,8428,6112],{"class":1268},[146,8430,1360],{"class":1214},[146,8432,5843],{"class":162},[146,8434,8435],{"class":1581}," res",[146,8437,6120],{"class":162},[146,8439,153],{"class":152},[146,8441,8442,8445,8447,8449,8452,8454,8456,8458,8460,8462,8464,8466,8469,8471,8473],{"class":148,"line":205},[146,8443,8444],{"class":1268},"        commit",[146,8446,1360],{"class":1214},[146,8448,1049],{"class":152},[146,8450,8451],{"class":175},"setUser",[146,8453,1049],{"class":152},[146,8455,1099],{"class":152},[146,8457,1059],{"class":152},[146,8459,7838],{"class":1214},[146,8461,169],{"class":152},[146,8463,6117],{"class":866},[146,8465,1176],{"class":152},[146,8467,8468],{"class":866},"data",[146,8470,1733],{"class":152},[146,8472,1384],{"class":1214},[146,8474,1052],{"class":152},[146,8476,8477,8479],{"class":148,"line":228},[146,8478,1521],{"class":152},[146,8480,6104],{"class":1214},[146,8482,8483,8485,8487,8489,8491,8493],{"class":148,"line":249},[146,8484,8426],{"class":152},[146,8486,6153],{"class":1268},[146,8488,1360],{"class":1214},[146,8490,5666],{"class":1581},[146,8492,6120],{"class":162},[146,8494,153],{"class":152},[146,8496,8497,8499,8501,8503,8505,8507],{"class":148,"line":270},[146,8498,6166],{"class":866},[146,8500,1176],{"class":152},[146,8502,6171],{"class":1268},[146,8504,1360],{"class":1214},[146,8506,5666],{"class":866},[146,8508,6104],{"class":1214},[146,8510,8511,8513],{"class":148,"line":284},[146,8512,1521],{"class":152},[146,8514,6104],{"class":1214},[146,8516,8517],{"class":148,"line":296},[146,8518,5717],{"class":152},[146,8520,8521],{"class":148,"line":302},[146,8522,347],{"class":152},[13,8524,8525],{},"nuxtClientInitを使用することでページ側の初期化処理より前にユーザー情報を取得できます。",[419,8527,8529],{"id":8528},"ssr","SSR",[13,8531,8532,8533,8536],{},"SSRの場合は今度は",[102,8534,8535],{},"NuxtServiceInit","に変更するだけです。これは特にライブラリは必要なく、SSRであれば利用できます。",[137,8538,8540],{"className":3069,"code":8539,"filename":8356,"language":3071,"meta":142,"style":142},"export const actions = {\n  async NuxtServiceInit({ commit }, context) {\n    \u002F\u002F サーバーサイドでトークンを入れるなどが必要な場合は適宜入れください。\n     await this.$axios.get('https:\u002F\u002Fapi.example.com\u002Fauth\u002Fme')\n    .then(async res=>{\n        commit('setUser', {user:res.data});\n    })\n    .catch(err=>{\n        console.error(err)\n    })\n  }\n}\n",[102,8541,8542,8554,8573,8578,8601,8617,8649,8655,8669,8683,8689,8693],{"__ignoreMap":142},[146,8543,8544,8546,8548,8550,8552],{"class":148,"line":149},[146,8545,7769],{"class":1027},[146,8547,7772],{"class":162},[146,8549,8367],{"class":866},[146,8551,1093],{"class":152},[146,8553,861],{"class":152},[146,8555,8556,8558,8561,8563,8565,8567,8569,8571],{"class":148,"line":156},[146,8557,8376],{"class":162},[146,8559,8560],{"class":1214}," NuxtServiceInit",[146,8562,8382],{"class":152},[146,8564,8385],{"class":1581},[146,8566,8388],{"class":152},[146,8568,8391],{"class":1581},[146,8570,1384],{"class":152},[146,8572,861],{"class":152},[146,8574,8575],{"class":148,"line":184},[146,8576,8577],{"class":1141},"    \u002F\u002F サーバーサイドでトークンを入れるなどが必要な場合は適宜入れください。\n",[146,8579,8580,8583,8585,8587,8589,8591,8593,8595,8597,8599],{"class":148,"line":199},[146,8581,8582],{"class":1027},"     await",[146,8584,8403],{"class":152},[146,8586,8406],{"class":866},[146,8588,1176],{"class":152},[146,8590,8411],{"class":1268},[146,8592,1360],{"class":1214},[146,8594,1049],{"class":152},[146,8596,8335],{"class":175},[146,8598,1049],{"class":152},[146,8600,6104],{"class":1214},[146,8602,8603,8605,8607,8609,8611,8613,8615],{"class":148,"line":205},[146,8604,8426],{"class":152},[146,8606,6112],{"class":1268},[146,8608,1360],{"class":1214},[146,8610,5843],{"class":162},[146,8612,8435],{"class":1581},[146,8614,6120],{"class":162},[146,8616,153],{"class":152},[146,8618,8619,8621,8623,8625,8627,8629,8631,8633,8635,8637,8639,8641,8643,8645,8647],{"class":148,"line":228},[146,8620,8444],{"class":1268},[146,8622,1360],{"class":1214},[146,8624,1049],{"class":152},[146,8626,8451],{"class":175},[146,8628,1049],{"class":152},[146,8630,1099],{"class":152},[146,8632,1059],{"class":152},[146,8634,7838],{"class":1214},[146,8636,169],{"class":152},[146,8638,6117],{"class":866},[146,8640,1176],{"class":152},[146,8642,8468],{"class":866},[146,8644,1733],{"class":152},[146,8646,1384],{"class":1214},[146,8648,1052],{"class":152},[146,8650,8651,8653],{"class":148,"line":249},[146,8652,1521],{"class":152},[146,8654,6104],{"class":1214},[146,8656,8657,8659,8661,8663,8665,8667],{"class":148,"line":270},[146,8658,8426],{"class":152},[146,8660,6153],{"class":1268},[146,8662,1360],{"class":1214},[146,8664,5666],{"class":1581},[146,8666,6120],{"class":162},[146,8668,153],{"class":152},[146,8670,8671,8673,8675,8677,8679,8681],{"class":148,"line":284},[146,8672,6166],{"class":866},[146,8674,1176],{"class":152},[146,8676,6171],{"class":1268},[146,8678,1360],{"class":1214},[146,8680,5666],{"class":866},[146,8682,6104],{"class":1214},[146,8684,8685,8687],{"class":148,"line":296},[146,8686,1521],{"class":152},[146,8688,6104],{"class":1214},[146,8690,8691],{"class":148,"line":302},[146,8692,5717],{"class":152},[146,8694,8695],{"class":148,"line":316},[146,8696,347],{"class":152},[13,8698,8699],{},"NuxtServiceInitというサーバーサイドで上記のリクエストを行って、storeにユーザー情報をいれてくれます。401が来てもcatchしてくれ、ユーザー情報はnullのままになります。",[46,8701,8702],{"id":8702},"完了",[13,8704,8705],{},"エッセンスは以下の通りです。",[398,8707,8708,8711,8714],{},[20,8709,8710],{},"初期処理にユーザー情報を取得するAPIをリクエスト",[20,8712,8713],{},"認証済みであればstoreのuserにユーザー情報のオブジェクトが入る",[20,8715,8716],{},"ミドルウェアやstoreの情報を使用して認証による分岐を行う",[908,8718,8719],{},"html pre.shiki code .s6cf3, html code.shiki .s6cf3{--shiki-default:#89DDFF;--shiki-default-font-style:italic}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s7ZW3, html code.shiki .s7ZW3{--shiki-default:#BABED8;--shiki-default-font-style:italic}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}",{"title":142,"searchDepth":184,"depth":184,"links":8721},[8722,8723,8724,8727,8731],{"id":7728,"depth":156,"text":7728},{"id":7754,"depth":156,"text":7755},{"id":7868,"depth":156,"text":7868,"children":8725},[8726],{"id":8109,"depth":184,"text":8110},{"id":8328,"depth":156,"text":8329,"children":8728},[8729,8730],{"id":8339,"depth":184,"text":8340},{"id":8528,"depth":184,"text":8529},{"id":8702,"depth":156,"text":8702},[930],"2022-05-19","フロント側の認証機能を実装します",{},"\u002Farticles\u002Fnuxt-auth-middleware",{"title":7697,"description":8734},"articles\u002Fnuxt-auth-middleware",[1763,8740],"nuxt","_common\u002Fnuxt.jpg","MWiO6wSm6JBqm1_bIaWiFdYFIqDsWKtpAvMLHvfsPGg",{"id":8744,"title":8745,"body":8746,"category":9046,"createdAt":9047,"description":9048,"extension":933,"index":934,"meta":9049,"navigation":936,"path":9050,"publish":936,"seo":9051,"series":934,"seriesTitle":934,"stem":9052,"tag":9053,"thumbnail":9056,"updatedAt":934,"__hash__":9057},"articles\u002Farticles\u002Fsecurity-incident-by-git-deplay.md","ドキュメントルート配下のコンテンツをGitを用いてデプロイする時のセキュリティ上の注意点",{"type":10,"value":8747,"toc":9032},[8748,8755,8758,8761,8778,8782,8785,8789,8796,8799,8802,8805,8808,8827,8833,8837,8847,8861,8864,8868,8871,8874,8877,8880,8884,8893,8899,8906,8909,8929,8944,8950,8954,8965,8968,8977,8980,8983,8989,8993,8999,9005,9008,9014,9017,9023,9026],[13,8749,8750,8751,8754],{},"こんにちはjunです。最近関わっているプロジェクトではGitを用いた本番環境での運用を行っています。本番環境にgitを置くことによって",[102,8752,8753],{},"git pull","するだけで改修したコードをすぐに反映できます。さらにブランチを分ければABテストや選択的なデプロイなども行えます。そのためシステム開発においてはGitによる本番環境での運用は欠かせません。",[13,8756,8757],{},"Laravelの様なシステム以外にも最近はwordpressのカスタムテーマやカスタムプラグインをgit管理し、本番環境にpullすることがあります。しかしその際の設定によってはプライベートのソースコードが流失したり、書き換えられたりなどセキュリティインシデントを犯しかねない設定になることがあります。私が公開前に止められたヒヤリハットとしてぜひ共有したいと思います。",[13,8759,8760],{},"今回解説するGitの管理と運用は以下のとおりです。",[17,8762,8763,8766,8769,8772,8775],{},[20,8764,8765],{},"プライベートリポジトリ",[20,8767,8768],{},"リモートはgithub",[20,8770,8771],{},"管理しているソースはwordpressのカスタムテーマ",[20,8773,8774],{},".gitがドキュメントルート配下にある",[20,8776,8777],{},"サーバはxserver（法人用レンタルサーバ）",[46,8779,8781],{"id":8780},"今回何が危なかったのか","今回何が危なかったのか？",[13,8783,8784],{},"まず結論から先に述べますと「パーソナルアクセストークンを用いたgit設定がドキュメントルート配下にあった」ことです。わかる人は結構やべーとわかるかもしれません。取り合えず解説していきます。",[419,8786,8788],{"id":8787},"パーソナルアクセストークンpatとは","パーソナルアクセストークン（PAT）とは",[13,8790,8791,8792],{},"パーソナルアクセストークンとはgithubにて使用されるパスワードの代わりとなるアクセストークンです。",[53,8793,3163],{"href":8794,"rel":8795},"https:\u002F\u002Fdocs.github.com\u002Fja\u002Fauthentication\u002Fkeeping-your-account-and-data-secure\u002Fcreating-a-personal-access-token",[57],[13,8797,8798],{},"一昔前はプライベートリポジトリにURL（HTTP）でアクセスする場合はドメインの部分にGithubアカウントのIDとパスワードを合わせる方法がとられていました（Basic認証）。ただしその方法はセキュリティ上危ないので、廃止され現在はパーソナルアクセストークン という予測がしづらく、行える操作スコープと使用期限が設定されたトークンを発行してパスワードの代わりに使用する様になりました。",[13,8800,8801],{},"そしてgitはpull,pushなどを行う際にそのorigin（リモートリポジトリ）のURL（HTTP）かSSHを使用します。SSHは環境によって難しかったり設定が大変なこともあり、PAT付きのURLを用いて接続すると簡単にプライベートリポジトリに接続ができます。基本的にパーソナルアクセストークン はこの様にgithubのアカウントが必要なプライベートリポジトリの読み取り、リポジトリ操作を行う際に使用します。",[13,8803,8804],{},"今回のテーマファイルのリポジトリはもちろんプライベートなので、トークン付きのURLかSSHで接続する必要があります。そして今回はPATをメインに使用して、gitの操作を行っていました。",[419,8806,8807],{"id":8807},"git設定ファイルとは",[13,8809,8810,8811,8814,8815,8818,8819,8822,8823,8826],{},"git設定ファイルとはリモートリポジトリ の接続先、ブランチ構成、ユーザーなどgit管理に必要な設定ファイルのことです。",[102,8812,8813],{},"git init","してローカルリポジトリ を作成した際に",[102,8816,8817],{},"ls -la","と打つと、",[102,8820,8821],{},".git","という隠しディレクトリが表示されます。それが設定ディレクトリであり、中に移動すると",[102,8824,8825],{},".git\u002Fconfig","という設定ファイルがあります。",[13,8828,8829,8830,8832],{},"先ほどのパーソナルアクセストークンを用いたURLなどはその",[102,8831,8825],{},"に書かれます。実際にlessやcatを使用してみてみると、リモートのURLなどが記載されています。",[419,8834,8836],{"id":8835},"なぜ読み取れた","なぜ読み取れた？",[13,8838,8839,8840,8843,8844,8846],{},"今回gitで運用しようとしていたwordpressのテーマファイルはドキュメントルート配下に設定します。ルートからみて　",[102,8841,8842],{},"\u002Fwp-content\u002Fthemes\u002Fcustom","というリポジトリ名にもなるテーマディレクトリを作成してそこに",[102,8845,8813],{},"をして接続先の情報を記載しました。上記のとおりPATを用いています。",[13,8848,8849,8850,638,8853,8856,8857,8860],{},"インフラに詳しい人ならわかると思いますが、基本的にドキュメントルート配下のファイルは",[102,8851,8852],{},".htaccess",[102,8854,8855],{},"Apache","で何かしら設定されていない場合、自由にみることができます。本来アクセスされない様なファイルにも例えば、",[102,8858,8859],{},"https:\u002F\u002Fexample.comm\u002Fwp-content\u002Fthemes\u002Fcustom\u002F.git\u002Fconfig","とURLでアクセスすると読み取れることがあります。",[13,8862,8863],{},"xserverの場合はconfigファイルがダウンロードされ、もちろん接続先情報は記載されていました。そのためパーソナルアクセストークン が記載されたgitの設定ファイルが第三者にダウンロード可能な状態で公開しうるとこでした。",[419,8865,8867],{"id":8866},"どうゆうことが起きかねる","どうゆうことが起きかねる？",[13,8869,8870],{},"仮に公開し、攻撃者がこの存在に気づくと何が起こり得るでしょうか？まず、パーソナルアクセストークン が盗まれリポジトリに対して不正アクセスされます。まだwebサーバを通じて設定ファイルを読み取っただけなので、gitコマンドを打たれて本番環境を破壊されることはないと思いますが、プライベートリポジトリに何かしらの攻撃をされるでしょう。本来プライベートなリポジトリ の情報を取得されてしまうのです。",[13,8872,8873],{},"またパーソナルアクセストークン には操作範囲を設定できます。今回のはリポジトリに対する読み書き操作でしたが、他にも管理者権限レベルの操作を付与できるスコープもあります。つまり、権限の強いトークンの場合はリポジトリ の内容が盗まれたり改変されるだけでなく、アカウント全体に影響が出かねないものになります。",[46,8875,8876],{"id":8876},"対策",[13,8878,8879],{},"ドキュメントルート配下にリポジトリを設定する場合は設定ファイルにアクセスできない様にする必要があります。またはPATを記載しないことです。",[419,8881,8883],{"id":8882},"gitディレクトリへのアクセスを404にする",".gitディレクトリへのアクセスを404にする",[13,8885,8886,8887,8889,8890,8892],{},"まず手取り早くできるのは",[102,8888,8852],{},"にて",[102,8891,8821],{},"を含むURLがあったら404にしてしまうことです。",[137,8894,8897],{"className":8895,"code":8896,"language":3289},[3287],"RedirectMatch 404 \u002F\\.git\n",[102,8898,8896],{"__ignoreMap":142},[13,8900,8901],{},[53,8902,8905],{"href":8903,"rel":8904},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F6142437\u002Fmake-git-directory-web-inaccessible",[57],"こちらのStacoverflowが役に立ちました。",[13,8907,8908],{},"解説によると",[17,8910,8911,8914,8923,8926],{},[20,8912,8913],{},"ドキュメントルート配下すべての.gitディレクトリ （設定ファイル）へのアクセスを禁止できる。つまり複数のリポジトリがあっても一気に対応可能。",[20,8915,8916,638,8919,8922],{},[102,8917,8918],{},".gitignore",[102,8920,8921],{},".gitmodules","にも対応できる。",[20,8924,8925],{},"これから新しく追加される.gitにも対応できる。",[20,8927,8928],{},"404にすることでリポジトリ の存在を悟らせない。",[13,8930,8931,8932,8934,8935,8937,8938,8940,8941,8943],{},"実際に",[102,8933,8852],{},"で設定すると、",[102,8936,8859],{},"へのアクセスは404になりました。上記の設定の場合はURLに",[102,8939,8821],{},"が含まれた瞬間、404になるのがコツです。さすがにないと思いますがURLに",[102,8942,8821],{},"を使用するコンテンツがある場合も404になるのでそこは注意が必要です。",[13,8945,8946,8947,8949],{},"webサーバによる読み取りとgitコマンドは関係ないので、上記の設定をしたとしても",[102,8948,8753],{},"など操作は引き続き使えます。",[419,8951,8953],{"id":8952},"sshに切り替える","SSHに切り替える",[13,8955,8956,8957,8960,8961,8964],{},"また可能であればSSHによる接続に切り替えることです。SSHによる接続のURLは",[102,8958,8959],{},"git@github.com:example:repository.git","となります。前半の",[102,8962,8963],{},"git@github.com","はsshのgitユーザーでgitub.comに接続するという意味です。そのユーザーで接続する際の秘密鍵やそのパスの設定はwebサーバーからは読み取ることができません。（上記のURLに直に書いてればべつだけど）",[13,8966,8967],{},"そのためパーソナルアクセストークンよりかはSSHでGitに接続するほうが安全性的にかなりベストです。PATより堅牢な方法です。",[13,8969,8970,8971,8976],{},"ちなみに本番環境からリポジトリへSSHで接続する際は",[53,8972,8975],{"href":8973,"rel":8974},"https:\u002F\u002Fdocs.github.com\u002Fja\u002Fauthentication\u002Fkeeping-your-account-and-data-secure\u002Freviewing-your-deploy-keys",[57],"デプロイキー","を使用するべきです。デプロイキーは特定のリポジトリのみのアクセスに限定するとともに、読み込み専用にすることができます。",[13,8978,8979],{},"PATでは読みに加えて書き込み権限と付与すればその他の管理者権限が使用できます。つまり盗まれた際の被害が甚大に対して、デプロイキーは特定のリポジトリ の読み込み権限のみを許可出るので被害が小さく済みます。",[419,8981,8982],{"id":8982},"対策まとめ",[13,8984,8985,8986,8988],{},"一番はwebサーバーで",[102,8987,8821],{},"に対するアクセス権限をなくすことです。これでパーソナルアクセストークンであっても外部から見られる心配はありません。ただし可能な限りまずはより安全なデプロイキーによるSSHで接続できる様にし、かつwebサーバの設定を行うことがベストです。",[46,8990,8992],{"id":8991},"うちは大丈夫チェック方法linux","うちは大丈夫？チェック方法(linux)",[13,8994,8995,8996,8998],{},"まずはドキュメントルート配下に",[102,8997,8821],{},"がいるかをチェックしましょう。Linuxの場合はドキュメントルート配下に以下のコマンドで探せます。",[137,9000,9003],{"className":9001,"code":9002,"language":3289},[3287],"find \u002FDOCUMENT\u002FROOT\u002F -name .git -type d\n",[102,9004,9002],{"__ignoreMap":142},[13,9006,9007],{},"もしあった場合",[137,9009,9012],{"className":9010,"code":9011,"language":3289},[3287],"\u002FDOCUMENT\u002FROOT\u002Fwp-content\u002Fthemes\u002Fexample\u002F.git\n",[102,9013,9011],{"__ignoreMap":142},[13,9015,9016],{},"このように結果が出てきますので、URLに載せて",[137,9018,9021],{"className":9019,"code":9020,"language":3289},[3287],"https:\u002F\u002Fexample.com\u002Fwp-content\u002Fthemes\u002Fexample\u002F.git\nhttps:\u002F\u002Fexample.com\u002Fwp-content\u002Fthemes\u002Fexample\u002F.git\u002Fconfig\n",[102,9022,9020],{"__ignoreMap":142},[13,9024,9025],{},"の２種類にアクセスしてディレクトリ 、configが閲覧されるかをチェックしましょう。サーバによっては対策済みの場合があります。",[13,9027,9028,9029,9031],{},"見れてしまったらまず",[102,9030,8852],{},"で閲覧できなくしましょう。中を見てパスワードやパーソナルアクセストークンがあったら今すぐ対処が必要です！！",{"title":142,"searchDepth":184,"depth":184,"links":9033},[9034,9040,9045],{"id":8780,"depth":156,"text":8781,"children":9035},[9036,9037,9038,9039],{"id":8787,"depth":184,"text":8788},{"id":8807,"depth":184,"text":8807},{"id":8835,"depth":184,"text":8836},{"id":8866,"depth":184,"text":8867},{"id":8876,"depth":156,"text":8876,"children":9041},[9042,9043,9044],{"id":8882,"depth":184,"text":8883},{"id":8952,"depth":184,"text":8953},{"id":8982,"depth":184,"text":8982},{"id":8991,"depth":156,"text":8992},[930],"2022-04-21","wordpressテーマやドキュメントルート配下にGitを置く際の注意点",{},"\u002Farticles\u002Fsecurity-incident-by-git-deplay",{"title":8745,"description":9048},"articles\u002Fsecurity-incident-by-git-deplay",[9054,941,9055],"security","git","_common\u002Fsecurity.jpg","zHB7HHf-61vEYna3ziFZoIxx8rS0KxIKcw1QiPSv20k",{"id":9059,"title":9060,"body":9061,"category":9526,"createdAt":9527,"description":9528,"extension":933,"index":934,"meta":9529,"navigation":936,"path":9530,"publish":936,"seo":9531,"series":934,"seriesTitle":934,"stem":9532,"tag":9533,"thumbnail":9535,"updatedAt":934,"__hash__":9536},"articles\u002Farticles\u002Flaravel-protect-resource.md","Laravelでログインしたユーザーのみ読み取れる画像・アセットを設定する方法",{"type":10,"value":9062,"toc":9519},[9063,9066,9077,9080,9088,9094,9097,9100,9108,9111,9132,9159,9162,9168,9171,9174,9203,9206,9220,9227,9230,9237,9240,9247,9276,9283,9310,9313,9316,9384,9396,9421,9436,9453,9456,9459,9462,9468,9503,9513,9516],[13,9064,9065],{},"こんにちはjunです。Laravel製のシステムにてWebマニュアルを作成していた時、「あれ？マニュルはログインユーザーのみ見れる様にするけど、画像などはどうすればいいんだ？」という事態がありました。",[13,9067,9068,9069,9072,9073,9076],{},"Laravelはルートを定義し、その際に認証を設けることができます。ただし静的な画像（今回の様なあらかじめセットしておくマニュアル画像など）を配置する場合は",[102,9070,9071],{},"public","配下または、",[102,9074,9075],{},"storage\u002Fpublic","配下に置くことが多いと思います。しかしそれらのディレクトリは名の通りいかなるアクセスに対してリクエストを許可しています。",[13,9078,9079],{},"そのため",[17,9081,9082,9085],{},[20,9083,9084],{},"ログインをしないと見れない画像やアセット",[20,9086,9087],{},"アクセスを制限したい画像やアセット",[13,9089,9090,9091,9093],{},"を実装したい時は単純に",[102,9092,9071],{},"配下に置くことはできません。この場合Laravelではコントローラーを使用して、アセットのリクエストに対して一度認証のロジックをかける必要があります。普段Laravelを使用していると、特定のURLとそのビューに対する認証はルートを定義するだけで簡単に設定できます。しかしビュー以外のアセットファイルの場合はWebサーバーとLaravelの仕組みを少し理解している必要ががります。今回はその様な保護したアセットルートの設定方法を解説しようと思います。",[46,9095,9096],{"id":9096},"概要",[13,9098,9099],{},"Laravelで構築されたURLで指定のビューやファイルをレスポンスとして返す時２通りの処理方法があります。",[398,9101,9102,9105],{},[20,9103,9104],{},"ドキュメントルート配下にリクエストで示されたファイルがある場合、それを返す。（webサーバー）",[20,9106,9107],{},"ドキュメントルートにない場合、Route.phpで定義したルートを照らし合わせ、設定したビューやファイルを返す。（webサーバー+PHP）",[13,9109,9110],{},"「２」の方はよくわかると思います。例えば以下の様なルートを定義した時",[137,9112,9116],{"className":9113,"code":9114,"filename":9115,"language":3355,"meta":142,"style":142},"language-php shiki shiki-themes material-theme-ocean","Route::get('\u002Ftest', function () {\n    return view('welcome');\n});\n","route.php",[102,9117,9118,9123,9128],{"__ignoreMap":142},[146,9119,9120],{"class":148,"line":149},[146,9121,9122],{},"Route::get('\u002Ftest', function () {\n",[146,9124,9125],{"class":148,"line":156},[146,9126,9127],{},"    return view('welcome');\n",[146,9129,9130],{"class":148,"line":184},[146,9131,3112],{},[13,9133,9134,9137,9138,9141,9142,9144,9145,1099,9148,9150,9151,9154,9155,9158],{},[102,9135,9136],{},"https:\u002F\u002Fexample.com\u002Ftest","というURLにアクセスすると",[102,9139,9140],{},"view('welcome')","で定義したHTML（画面）がレスポンスとして表示されます。一方「１」の方はというと例えば",[102,9143,9071],{},"配下に置いた",[102,9146,9147],{},"css",[102,9149,1763],{},"など静的なファイルがあげられます。例えば",[102,9152,9153],{},"https:\u002F\u002Fexample.com\u002Fcss\u002Fstyle.css","の場合、webサーバーは",[102,9156,9157],{},"public\u002Fcss\u002Fstyle.css","があればそれをレスポンスとして返します。",[13,9160,9161],{},"両者の違いはwebサーバーだけで完結しているか、PHP（Laravel）も動かしているかです。これはpublic配下の.htaccessを見ると理解できます。",[137,9163,9166],{"className":9164,"code":9165,"filename":8852,"language":3289,"meta":142},[3287],"\u003CIfModule mod_rewrite.c>\n    # Send Requests To Front Controller...\n    RewriteCond %{REQUEST_FILENAME} !-d\n    RewriteCond %{REQUEST_FILENAME} !-f\n    RewriteRule ^ index.php [L]\n\u003C\u002FIfModule>\n",[102,9167,9165],{"__ignoreMap":142},[13,9169,9170],{},"一部省略していますが、重要なのはこの箇所です。これは「もし、リクエストしたディレクトリおよびファイルがない場合、index.phpを実行する」という意味です。つまりLaravelが置かれたwebサーバーでは、まず「リクエストされたファイルが静的に置かれているかをチェック」そしてもしない場合は「index.phpを実行してLaravelが動的にルートに対するレスポンスを作成する」ということが行われています。",[13,9172,9173],{},"Apache側で静的ファイルに対するアクセスを設定していない場合、基本的にリクエストに合致するファイルがある場合はレスポンスしてしまいます。今回の様な保護したファイル、つまりLaravelの認証などを通してファイルを返すためには、独自のルートを定義してレスポンスする必要があります。",[39,9175,9177,9178,9181,9182,9185,9186,9188,9189,9192,9193],{"className":9176},[42,594],"\nLaravelではアップロードされたファイルは",[102,9179,9180],{},"storage\u002Fapp\u002Fpublic"," 配下に配置し、そのstorageファイルへのリクエストは上記のwebサーバーのみで処理できます。storageディレクトリがpublicとは別なのになぜできるのか？それはシンボリックリンクを張っているからです。構築時に",[102,9183,9184],{},"php artisan storage:link","というおまじないを唱えたと思います。これはpublic配下に",[102,9187,9180],{},"に連絡する",[102,9190,9191],{},"storage","というシンボリックリンクを配置する処理を行っています。\n",[13,9194,9195,9196,9199,9200,9202],{},"実際にpublic配下にstorageというものがあり、Vscodeでは矢印マークが加わっているのが分かります。ls -lを実行してみると",[102,9197,9198],{},"storage -> \u002Fvar\u002Fwww\u002Fhtml\u002Fstorage\u002Fapp\u002Fpublic","という風に表示されます（私の環境の場合）。ディレクトリとして離れていても、シンボリックリンクを貼ることで",[102,9201,9180],{},"配下をwebサーバーが走査することができる様にしています。",[13,9204,9205],{},"今回のような保護したアセットファイルルートを設定するために",[398,9207,9208,9211,9214,9217],{},[20,9209,9210],{},"専用のディレクトリを作成",[20,9212,9213],{},"読み取りルートの定義",[20,9215,9216],{},"ファイルの取得処理",[20,9218,9219],{},"レスポンス処理",[13,9221,9222,9223,9226],{},"上記のプログラムを作成します。今回は「ログインしたユーザーが見れるwebマニュアルの画像」ということなので、",[102,9224,9225],{},"resources","配下にファイルを置いておくことにします。一応後でstorageディレクトリに保護ファイルを配置・取得する方法も記述します。",[46,9228,9229],{"id":9229},"ディレクトリの作成",[13,9231,9232,9233,9236],{},"まずは専用のディレクトリを作成します。今回は静的に置いておくので",[102,9234,9235],{},"resources\u002Fprotected","という保護アセットファイルディレクトリを作っておきます。リクエストがあった場合はこのディレクトリからファイルを取得します。",[46,9238,9239],{"id":9239},"ルートを定義する",[13,9241,9242,9243,9246],{},"それではルートを定義します。",[102,9244,9245],{},"routes\u002Fweb.php","にて以下の様なルートを設定。",[137,9248,9250],{"className":9113,"code":9249,"filename":9245,"language":3355,"meta":142,"style":142},"Route::group(['middleware' => 'auth'], function () {\n    Route::get('\u002Fprotected\u002F{path?}', function (Request $request,$path='') {\n        \u002F\u002F 後で書きます..\n    })->where('path', '.*');\n});\n",[102,9251,9252,9257,9262,9267,9272],{"__ignoreMap":142},[146,9253,9254],{"class":148,"line":149},[146,9255,9256],{},"Route::group(['middleware' => 'auth'], function () {\n",[146,9258,9259],{"class":148,"line":156},[146,9260,9261],{},"    Route::get('\u002Fprotected\u002F{path?}', function (Request $request,$path='') {\n",[146,9263,9264],{"class":148,"line":184},[146,9265,9266],{},"        \u002F\u002F 後で書きます..\n",[146,9268,9269],{"class":148,"line":199},[146,9270,9271],{},"    })->where('path', '.*');\n",[146,9273,9274],{"class":148,"line":205},[146,9275,3112],{},[13,9277,9278,9279,9282],{},"authミドルウェアでグルーピングをして",[102,9280,9281],{},"protected","配下のルートを保護します。",[13,9284,9285,9288,9289,1099,9292,9295,9296,9298,9299,9302,9303,1099,9306,9309],{},[102,9286,9287],{},"{path?}","は任意の記述を意味します。つまり",[102,9290,9291],{},"\u002Fprotected\u002Fsecret.jpg",[102,9293,9294],{},"\u002Fprotected\u002Fmanual\u002Fprivate.png","などのルートにをキャッチすることができます。そして",[102,9297,9287],{},"パラメータはコールバック（コントローラー）に第二引数として使用できます。先程の例のパスの場合、",[102,9300,9301],{},"$path","は",[102,9304,9305],{},"secret.jpg",[102,9307,9308],{},"manual\u002Fprivate.png","となります。この値は後でファイルの取得に使用します。ちなみに今回はルートに処理内容を記述しますが、プロジェクトによっては複雑な認証処理を実装する場合はコントローラーに記述しても大丈夫です。",[46,9311,9312],{"id":9312},"ファイルの取得とレスポンスを行う",[13,9314,9315],{},"ではファイルの取得の処理を記述します。",[137,9317,9319],{"className":9113,"code":9318,"filename":9245,"language":3355,"meta":142,"style":142},"use Illuminate\\Support\\Facades\\File;\n\nRoute::group(['middleware' => 'auth'], function () {\n    Route::get('\u002Fprotected\u002F{path?}', function (Request $request,$path='') {\n        if($path==='') abort(404);\n\n        $rp = resource_path('protected\u002F'.$path);\n        if(File::exists($rp)){\n            return response()->file($rp);\n        }else{\n            abort(404);\n        }\n    })->where('path', '.*');\n});\n",[102,9320,9321,9326,9330,9334,9338,9343,9347,9352,9357,9362,9367,9372,9376,9380],{"__ignoreMap":142},[146,9322,9323],{"class":148,"line":149},[146,9324,9325],{},"use Illuminate\\Support\\Facades\\File;\n",[146,9327,9328],{"class":148,"line":156},[146,9329,1082],{"emptyLinePlaceholder":936},[146,9331,9332],{"class":148,"line":184},[146,9333,9256],{},[146,9335,9336],{"class":148,"line":199},[146,9337,9261],{},[146,9339,9340],{"class":148,"line":205},[146,9341,9342],{},"        if($path==='') abort(404);\n",[146,9344,9345],{"class":148,"line":228},[146,9346,1082],{"emptyLinePlaceholder":936},[146,9348,9349],{"class":148,"line":249},[146,9350,9351],{},"        $rp = resource_path('protected\u002F'.$path);\n",[146,9353,9354],{"class":148,"line":270},[146,9355,9356],{},"        if(File::exists($rp)){\n",[146,9358,9359],{"class":148,"line":284},[146,9360,9361],{},"            return response()->file($rp);\n",[146,9363,9364],{"class":148,"line":296},[146,9365,9366],{},"        }else{\n",[146,9368,9369],{"class":148,"line":302},[146,9370,9371],{},"            abort(404);\n",[146,9373,9374],{"class":148,"line":316},[146,9375,335],{},[146,9377,9378],{"class":148,"line":326},[146,9379,9271],{},[146,9381,9382],{"class":148,"line":332},[146,9383,3112],{},[13,9385,9386,9387,9389,9390,8889,9393,9395],{},"最初に",[102,9388,9301],{},"がない場合は404にアボートします。そして何かしらファイルが指定された場合は",[102,9391,9392],{},"resource_path('protected\u002F'.$path)",[102,9394,9235],{},"配下のファイルパスを取得します。",[39,9397,9399,9400,9403,9404,638,9407,9410,9411,9414,9415,9302,9417,9420],{"className":9398},[42,43],"\nwebサーバーでなくPHP（Laravel）にてユーザーからの入力値（リクエストパス）を用いてファイルの取得をする場合、PHPの",[102,9401,9402],{},"file_get_contents()","は使用せずLaravelの",[102,9405,9406],{},"resource_path()",[102,9408,9409],{},"storage_path","を使用し、さらにFileファサード、",[102,9412,9413],{},"file()","メソッドを使用しましょう。",[102,9416,9402],{},[102,9418,9419],{},"..\u002F","といった記述は文字列でなく、パスとして認識してしまい想定しないディレクトリのファイルにがブラウザを通じて取得される可能性があります。このような脆弱性をディレクトリトラバーサルといいます。Laravelのファイル取得系のメソッドはその辺は対策済みなので、基本的にはLaravelのメソッドを使用しましょう。\n",[13,9422,9423,9424,9427,9428,9431,9432,9435],{},"ファイルパスを作成したら",[102,9425,9426],{},"File::exists()"," を使用してファイルが存在するかをチェックします。存在しないファイルをfile()パスで使用すると",[102,9429,9430],{},"FileNotFoundException","が発生してしまいます。例外処理でやってもいいのですが、",[102,9433,9434],{},"response","メソッドを呼んでいるので念のためあらかじめチェックしておきます。",[13,9437,9438,9439,9442,9443,9445,9446,9448,9449,9452],{},"ファイルがある場合は ",[102,9440,9441],{},"response()->file();","を使用して対象の",[102,9444,9235],{},"配下のファイルをレスポンスとして返します。ない場合は404へアボートします。",[102,9447,9413],{},"メソッドを使用することで拡張子から",[102,9450,9451],{},"content-type","を設定してくれるそうでCSVだろうがHTMLでもMP4でも問題なくレスポンスしてくれます。",[13,9454,9455],{},"ルート自体はLaravelのミドルウェアを使用することでファイルを保護し、任意のファイルパスを使用して認証が通ればファイルを取得することができる様になります。",[46,9457,9458],{"id":9458},"storageでやる方法",[13,9460,9461],{},"resourcesは基本的に開発者が静的にファイルを置く場合に使用します。ユーザーが自由にアップロードして、保護しながら呼び出したい時はstorageディレクトリを使用します。ファイルの取得と保護は上記とほぼ同じですが、storageの場合は少しcconfigの設定を行います。",[13,9463,9464,9467],{},[102,9465,9466],{},"config\u002Ffilesystem.php","にて以下の様に保護storageディレクトリを定義します。",[137,9469,9471],{"className":9113,"code":9470,"filename":9466,"language":3355,"meta":142,"style":142},"'protected' => [\n    'driver' => 'local',\n    'root' => storage_path('app\u002Fprotected'),\n    'url' => env('APP_URL') . '\u002Fstorage',\n    'visibility' => 'private',\n],\n\n",[102,9472,9473,9478,9483,9488,9493,9498],{"__ignoreMap":142},[146,9474,9475],{"class":148,"line":149},[146,9476,9477],{},"'protected' => [\n",[146,9479,9480],{"class":148,"line":156},[146,9481,9482],{},"    'driver' => 'local',\n",[146,9484,9485],{"class":148,"line":184},[146,9486,9487],{},"    'root' => storage_path('app\u002Fprotected'),\n",[146,9489,9490],{"class":148,"line":199},[146,9491,9492],{},"    'url' => env('APP_URL') . '\u002Fstorage',\n",[146,9494,9495],{"class":148,"line":205},[146,9496,9497],{},"    'visibility' => 'private',\n",[146,9499,9500],{"class":148,"line":228},[146,9501,9502],{},"],\n",[13,9504,9505,9506,9509,9510,9512],{},"こうすることで",[102,9507,9508],{},"Storage::disk('protected')->path()","を用いて対象ファイルパスを取得することができる様になります。ファイルストレージはローカルでなくS3など外部のものを使用することもあるので、この様に設定ファイルで定義しておくといいです。storageにprotectedディレクトリを作成した後、あとはルートを定義して",[102,9511,9508],{},"を用いてリクエストされたファイルパスを取得し、存在チェックをしてレスポンスで返せばOKです。",[13,9514,9515],{},"今回は簡単なauthミドルウェアですが、権限のロジックを組み込むことで所有者のリクエストのみに見せたり、特定の人のみに見せるといった芸当ができそうです。ただしwebサーバーの静的な配信でなく、ファイルの取得にPHPを動かすことになるので大量配信の場合はパフォーマンスはちょっと心配かもしません。",[908,9517,9518],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":142,"searchDepth":184,"depth":184,"links":9520},[9521,9522,9523,9524,9525],{"id":9096,"depth":156,"text":9096},{"id":9229,"depth":156,"text":9229},{"id":9239,"depth":156,"text":9239},{"id":9312,"depth":156,"text":9312},{"id":9458,"depth":156,"text":9458},[930],"2022-03-26","画像、アセットにログインしたユーザーのみリクエストを制限する",{},"\u002Farticles\u002Flaravel-protect-resource",{"title":9060,"description":9528},"articles\u002Flaravel-protect-resource",[9534,3355],"laravel","_common\u002Flaravel.png","f9NQ8_uhPD5HoW5tFBCWKT-Mw4ExKqCgzTqezzlXg_w",1780987140583]