diff --git a/README.md b/README.md index a0ee0b7..5a1aa30 100644 --- a/README.md +++ b/README.md @@ -4,5 +4,5 @@ ```bash $ docker-compose up -d -$ docker-compose exec app bundle exec rails db:create +$ docker-compose exec app bundle exec rails db:setup ``` diff --git a/app/assets/stylesheets/records.scss b/app/assets/stylesheets/records.scss new file mode 100644 index 0000000..9ec3201 --- /dev/null +++ b/app/assets/stylesheets/records.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Records controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/records_controller.rb b/app/controllers/records_controller.rb new file mode 100644 index 0000000..1caf4c1 --- /dev/null +++ b/app/controllers/records_controller.rb @@ -0,0 +1,16 @@ +class RecordsController < ApplicationController + def index + @records = Record.all + @record = Record.new + end + + def create + record_params = params.require(:record).permit(:voice) + @record = Record.new(record_params) + if @record.save + head :created + else + head :unprocessable_entity + end + end +end diff --git a/app/helpers/records_helper.rb b/app/helpers/records_helper.rb new file mode 100644 index 0000000..e63c599 --- /dev/null +++ b/app/helpers/records_helper.rb @@ -0,0 +1,2 @@ +module RecordsHelper +end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9cd55d4..feba9c7 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -3,11 +3,15 @@ // a relevant structure within app/javascript and only use these pack files to reference // that code so it'll be compiled. -require("@rails/ujs").start() -require("turbolinks").start() -require("@rails/activestorage").start() -require("channels") - +require("@rails/ujs").start(); +require("turbolinks").start(); +require("@rails/activestorage").start(); +require("channels"); +const OpusMediaRecorder = require("opus-media-recorder"); +const Worker = require("opus-media-recorder/encoderWorker.js"); +const OggOpusWasm = require("opus-media-recorder/OggOpusEncoder.wasm"); +const WebMOpusWasm = require("opus-media-recorder/WebMOpusEncoder.wasm"); +const axios = require("axios"); // Uncomment to copy all static images under ../images to the output folder and reference // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) @@ -15,3 +19,89 @@ require("channels") // // const images = require.context('../images', true) // const imagePath = (name) => images(name, true) + +// Polyfill +window.MediaRecorder = OpusMediaRecorder; + +document.addEventListener("turbolinks:load", init); + +function init() { + if (!navigator.mediaDevices.getUserMedia) return; + + const soundClips = document.querySelector("#js-sound-clips"); + const startButton = document.querySelector("#js-start-recording"); + const stopButton = document.querySelector("#js-stop-recording"); + + startButton.disabled = false; + stopButton.disabled = true; + + const constraints = { audio: true }; + let chunks = []; + + // iOS Safari は ogg の再生ができない + // このサンプルでは容量が大きくなってもやむなしとして wav で + const mimeType = "audio/wav"; + + const onSuccess = stream => { + const options = { + mimeType: mimeType + }; + const workerOptions = { + encoderWorkerFactory: _ => new Worker(), + OggOpusEncoderWasmPath: OggOpusWasm, + WebMOpusEncoderWasmPath: WebMOpusWasm + }; + const mediaRecorder = new MediaRecorder(stream, options, workerOptions); + + startButton.addEventListener("click", () => { + mediaRecorder.start(); + console.log(mediaRecorder.state); + console.log("recorder started"); + startButton.disabled = true; + stopButton.disabled = false; + }); + + stopButton.addEventListener("click", () => { + mediaRecorder.stop(); + console.log(mediaRecorder.state); + console.log("recorder stopped"); + startButton.disabled = false; + stopButton.disabled = true; + }); + + mediaRecorder.onstop = e => { + console.log("data available after MediaRecorder.stop() called."); + + var blob = new Blob(chunks, { type: mimeType }); + chunks = []; + + var uploadFormData = new FormData(); + var csrfParam = document.querySelector("meta[name='csrf-param']").content; + var csrfToken = document.querySelector("meta[name='csrf-token']").content; + uploadFormData.append(csrfParam, csrfToken); + uploadFormData.append("record[voice]", blob); + + axios + .post("/records", uploadFormData) + .then(result => { + console.log(result); + window.location.reload(true); + }) + .catch(error => { + console.log(error); + }); + + console.log("recorder stopped"); + }; + + mediaRecorder.ondataavailable = e => { + chunks.push(e.data); + }; + }; + + var onError = err => { + console.log("The following error occured: " + err); + }; + + navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); +} diff --git a/app/models/record.rb b/app/models/record.rb new file mode 100644 index 0000000..13dfd2a --- /dev/null +++ b/app/models/record.rb @@ -0,0 +1,3 @@ +class Record < ApplicationRecord + has_one_attached :voice +end diff --git a/app/views/records/index.html.erb b/app/views/records/index.html.erb new file mode 100644 index 0000000..735d80d --- /dev/null +++ b/app/views/records/index.html.erb @@ -0,0 +1,14 @@ +

Records#index

+

Find me in app/views/records/index.html.erb

+ +
+ + +
+ +<% @records.each do |record| %> +
+

<%= record.created_at %>

+ <%= audio_tag rails_blob_url(record.voice, disposition: "attachment"), controls: true %> +
+<% end %> diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index dc18996..b89d91b 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -2,3 +2,6 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf + +# public assets +Rack::Mime::MIME_TYPES[".wasm"] = "application/wasm" diff --git a/config/routes.rb b/config/routes.rb index c06383a..28a0ca0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,4 @@ Rails.application.routes.draw do - # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html + root "records#index" + resources :records end diff --git a/config/webpack/environment.js b/config/webpack/environment.js index d16d9af..ac9101b 100644 --- a/config/webpack/environment.js +++ b/config/webpack/environment.js @@ -1,3 +1,22 @@ -const { environment } = require('@rails/webpacker') +const { environment } = require("@rails/webpacker"); -module.exports = environment +const opusMediaRecorderEncoderWorkerLoader = { + test: /opus-media-recorder\/encoderWorker\.js$/, + loader: "worker-loader" +}; +const opusMediaRecorderWasmLoader = { + test: /opus-media-recorder\/.*\.wasm$/, + type: "javascript/auto", + loader: "file-loader" +}; + +environment.loaders.prepend( + "opusMediaRecorderEncoderWorkerLoader", + opusMediaRecorderEncoderWorkerLoader +); +environment.loaders.prepend( + "opusMediaRecorderWasmLoader", + opusMediaRecorderWasmLoader +); + +module.exports = environment; diff --git a/db/migrate/20190703125820_create_active_storage_tables.active_storage.rb b/db/migrate/20190703125820_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000..0b2ce25 --- /dev/null +++ b/db/migrate/20190703125820_create_active_storage_tables.active_storage.rb @@ -0,0 +1,27 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[5.2] + def change + create_table :active_storage_blobs do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.bigint :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false + t.references :blob, null: false + + t.datetime :created_at, null: false + + t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end +end diff --git a/db/migrate/20190703130233_create_records.rb b/db/migrate/20190703130233_create_records.rb new file mode 100644 index 0000000..f4c61f7 --- /dev/null +++ b/db/migrate/20190703130233_create_records.rb @@ -0,0 +1,8 @@ +class CreateRecords < ActiveRecord::Migration[6.0] + def change + create_table :records do |t| + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..45e2705 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,45 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2019_07_03_130233) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.bigint "byte_size", null: false + t.string "checksum", null: false + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "records", force: :cascade do |t| + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" +end diff --git a/package.json b/package.json index 459889f..7b17893 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,13 @@ "@rails/activestorage": "^6.0.0-alpha", "@rails/ujs": "^6.0.0-alpha", "@rails/webpacker": "^4.0.7", + "axios": "^0.19.0", "turbolinks": "^5.2.0" }, "version": "0.1.0", "devDependencies": { - "webpack-dev-server": "^3.7.2" + "opus-media-recorder": "^0.7.19", + "webpack-dev-server": "^3.7.2", + "worker-loader": "^2.0.0" } } diff --git a/spec/controllers/records_controller_spec.rb b/spec/controllers/records_controller_spec.rb new file mode 100644 index 0000000..c629c9b --- /dev/null +++ b/spec/controllers/records_controller_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +RSpec.describe RecordsController, type: :controller do + + describe "GET #index" do + it "returns http success" do + get :index + expect(response).to have_http_status(:success) + end + end + +end diff --git a/spec/helpers/records_helper_spec.rb b/spec/helpers/records_helper_spec.rb new file mode 100644 index 0000000..6ab29f1 --- /dev/null +++ b/spec/helpers/records_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the RecordsHelper. For example: +# +# describe RecordsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe RecordsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/record_spec.rb b/spec/models/record_spec.rb new file mode 100644 index 0000000..50334b9 --- /dev/null +++ b/spec/models/record_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Record, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/views/records/index.html.erb_spec.rb b/spec/views/records/index.html.erb_spec.rb new file mode 100644 index 0000000..74b83a2 --- /dev/null +++ b/spec/views/records/index.html.erb_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe "records/index.html.erb", type: :view do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/yarn.lock b/yarn.lock index 2b989d7..1203671 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1160,6 +1160,14 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axios@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" + integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== + dependencies: + follow-redirects "1.5.10" + is-buffer "^2.0.2" + babel-loader@^8.0.6: version "8.0.6" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb" @@ -2137,6 +2145,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -2249,6 +2264,11 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +detect-browser@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-4.5.1.tgz#b9df3f66454a4f32adbc4db2949aa788b757921b" + integrity sha512-cGXvbxvDws+ZjzR3AI+2IcKQR3Tj85PaUn42u6A/DWOEYda5fgvkS/NrQp2lD4LZ/IE2nLE/0kV//qekOyxJ2Q== + detect-file@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" @@ -2483,6 +2503,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-target-shim@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-3.0.2.tgz#af25bb55a97c670bbeba985c82647fb64d892153" + integrity sha512-HK5GhnEAkm7fLy249GtF7DIuYmjLm85Ft6ssj7DhVl8Tx/z9+v0W6aiIVUdT4AXWGYy5Fc+s6gqBI49Bf0LejQ== + eventemitter3@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" @@ -2733,6 +2758,13 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + follow-redirects@^1.0.0: version "1.7.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" @@ -3409,6 +3441,11 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-buffer@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" + integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== + is-callable@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" @@ -3793,7 +3830,7 @@ loader-runner@^2.3.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.2.3, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: +loader-utils@1.2.3, loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== @@ -4532,6 +4569,14 @@ optimize-css-assets-webpack-plugin@^5.0.1: cssnano "^4.1.10" last-call-webpack-plugin "^3.0.0" +opus-media-recorder@^0.7.19: + version "0.7.19" + resolved "https://registry.yarnpkg.com/opus-media-recorder/-/opus-media-recorder-0.7.19.tgz#47696cd5723ae24b6242bba91dd98f7c2a99dc1b" + integrity sha512-5KmT/dSEuonD4GEZWzmfnqoAuYexcgiDlQUQhtYneTorKDhZDHQ0fS9gQe00bOWyyIumBZMMUithTQD6+dr74g== + dependencies: + detect-browser "^4.1.0" + event-target-shim "^3.0.2" + original@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" @@ -5939,6 +5984,14 @@ sax@^1.2.4, sax@~1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +schema-utils@^0.4.0: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -7053,6 +7106,14 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac" + integrity sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw== + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.4.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"