Jelajahi Sumber

音频完整下载后播放

Michael Wang 5 tahun lalu
induk
melakukan
8049393ce0

+ 2 - 1
package.json

@@ -9,9 +9,10 @@
     "mock": "json-server --watch mock/db.json",
     "prebuild:staging": "node prebuild",
     "build:staging": "NODE_ENV=production vue-cli-service build --mode staging",
+    "postbuild:staging": "node postbuild",
     "prebuild:prod": "node prebuild",
     "build:prod": "vue-cli-service build",
-    "postbuild:prod": "node postbuild",
+    "postbuild:prod": "IS_PROD=true node postbuild",
     "test:unit": "vue-cli-service test:unit"
   },
   "eslintIgnore": [

+ 36 - 21
postbuild.js

@@ -1,25 +1,40 @@
 console.log("> postbuild");
-console.log("> postbuild 将sourcemap文件改名,防止源码泄露");
-
-const revision = require("child_process")
-  .execSync("git rev-parse HEAD")
-  .toString()
-  .trim()
-  .slice(10, 17);
 const fs = require("fs");
-const DIR = "./dist/js/";
-const sourcemaps = fs
-  .readdirSync(DIR)
-  .filter(v => v.endsWith(".map"))
-  .map(v => DIR + v);
-for (const s of sourcemaps) {
-  fs.renameSync(s, s.replace(".js.map", "-" + revision + ".js.map"));
-  console.log(
-    "  rename ",
-    s,
-    " => ",
-    s.replace(".js.map", "-" + revision + ".js.map")
-  );
+
+console.log("process.env.IS_PROD", process.env.IS_PROD);
+
+if (process.env.IS_PROD === "true") {
+  console.log("> postbuild 将sourcemap文件改名,防止源码泄露");
+
+  const revision = require("child_process")
+    .execSync("git rev-parse HEAD")
+    .toString()
+    .trim()
+    .slice(10, 17);
+  const DIR = "./dist/js/";
+  const sourcemaps = fs
+    .readdirSync(DIR)
+    .filter(v => v.endsWith(".map"))
+    .map(v => DIR + v);
+  for (const s of sourcemaps) {
+    fs.renameSync(s, s.replace(".js.map", "-" + revision + ".js.map"));
+    console.log(
+      "  rename ",
+      s,
+      " => ",
+      s.replace(".js.map", "-" + revision + ".js.map")
+    );
+  }
+
+  console.log();
 }
 
-console.log();
+console.log(
+  "> postbuild 将serviceWorkerAppend.js 附加到 dist/service-worker.js中"
+);
+
+const serviceWorkerAppend = fs.readFileSync("./serviceWorkerAppend.js", {
+  encoding: "utf-8",
+});
+
+fs.appendFileSync("./dist/service-worker.js", serviceWorkerAppend);

+ 101 - 0
serviceWorkerAppend.js

@@ -0,0 +1,101 @@
+/** append by ecs */
+// 以下为学生端添加内容
+
+self.addEventListener("fetch", event => {
+  // Prevent the default, and handle the request ourselves.
+  event.respondWith(
+    (async function() {
+      // console.log("fetch intercepted");
+      // Try to get the response from a cache.
+      const cachedResponse = await caches.match(event.request);
+      // Return it if we found one.
+      if (cachedResponse) {
+        console.log("cache res", event.request.url);
+        return cachedResponse;
+      }
+
+      if (
+        event.request.url.startsWith(
+          "https://ecs-test-static.qmth.com.cn/comm-ques-bank/dev/audio"
+        ) ||
+        event.request.url.startsWith(
+          "https://ecs-static.qmth.com.cn/comm-ques-bank/prod/audio/"
+        )
+      ) {
+        console.log("fetch audio intercept, try to load all");
+        const res = await fetch(event.request.url);
+
+        const reader = res.body.getReader();
+
+        // Step 2: get total length
+        const contentLength = +res.headers.get("Content-Length");
+        // console.log("content-length", contentLength);
+        // self.sessionStorage.setItem("test-sw", "1");
+        // self.postMessage({
+        //   url: event.request.url,
+        //   progress: 1,
+        // });
+
+        // self.addEventListener('message', function(event){
+        //   console.log("SW Received Message: " + event.data);
+        // console.log(event);
+        // event.ports[0].postMessage("SW Says 'Hello back!'");
+        // });
+
+        // Step 3: read the data
+        let receivedLength = 0; // received that many bytes at the moment
+        let chunks = []; // array of received binary chunks (comprises the body)
+        while (true) {
+          const { done, value } = await reader.read();
+
+          if (done) {
+            break;
+          }
+
+          chunks.push(value);
+          receivedLength += value.length;
+
+          // console.log(`Received ${receivedLength} of ${contentLength}`);
+
+          self.clients.matchAll().then(function(clients) {
+            if (clients && clients.length) {
+              clients.forEach(function(client) {
+                client.postMessage([
+                  event.request.url,
+                  receivedLength,
+                  contentLength,
+                ]);
+              });
+            }
+          });
+        }
+
+        // Step 4: concatenate chunks into single Uint8Array
+        // let chunksAll = new Uint8Array(receivedLength); // (4.1)
+        // let position = 0;
+        // for (let chunk of chunks) {
+        //   chunksAll.set(chunk, position); // (4.2)
+        //   position += chunk.length;
+        // }
+
+        // const blob = await res.clone().blob();
+        // const blob = new Blob([chunksAll.buffer]);
+        const blob = new Blob(chunks);
+        const responseFinal = new Response(blob, {
+          headers: {
+            "Content-Type": "audio/mp3",
+            "Content-Length": contentLength,
+          },
+        });
+
+        caches.open("audios").then(function(cache) {
+          cache.put(event.request, responseFinal.clone());
+        });
+
+        return responseFinal.clone();
+      }
+      // If we didn't find a match in the cache, use the network.
+      return fetch(event.request);
+    })()
+  );
+});

+ 9 - 0
src/features/OnlineExam/Examing/ExamingEnd.vue

@@ -169,6 +169,15 @@ export default {
       questionAnswerFileUrl: [],
       pictureAnswer: {},
     });
+
+    // 清除考试过程中存储的音频
+    caches.open("audios").then(function(cache) {
+      cache.keys().then(function(keys) {
+        keys.forEach(function(request) {
+          cache.delete(request);
+        });
+      });
+    });
   },
   computed: {
     ...globalMapState(["user"]),

+ 73 - 0
src/features/OnlineExam/Examing/QuestionAudio.vue

@@ -0,0 +1,73 @@
+<template>
+  <span>
+    <audio
+      v-show="shouldShowAudio"
+      controls
+      preload="auto"
+      controlsList="nodownload"
+      :key="src"
+      :name="name"
+      :src="src"
+      v-on="$listeners"
+      style="width: 240px"
+    ></audio>
+
+    <span v-if="!shouldShowAudio" style="color: blueviolet;">
+      音频下载中{{ downloadPercent }}%
+    </span>
+  </span>
+</template>
+
+<script>
+export default {
+  name: "QuestionAudio",
+  props: {
+    src: {
+      type: String,
+      required: true,
+    },
+    name: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      done: false,
+      downloadPercent: 0,
+    };
+  },
+  async created() {
+    this.cacheInterval = setInterval(() => {
+      if (!this.done) {
+        caches
+          .open("audios")
+          .then(caches => caches.match(this.src))
+          .then(e => e && (this.done = true));
+      }
+    }, 100);
+
+    this.handleMSG = event => {
+      // console.log("Client 1 Received Message: " + event.data);
+      if (event.data[0] === this.src)
+        this.downloadPercent = Math.floor(
+          (event.data[1] / event.data[2]) * 100
+        );
+    };
+    navigator.serviceWorker.addEventListener("message", this.handleMSG);
+  },
+  beforeDestroy() {
+    clearInterval(this.cacheInterval);
+    navigator.serviceWorker.removeEventListener("message", this.handleMSG);
+  },
+  computed: {
+    shouldShowAudio() {
+      return (
+        this.done ||
+        !navigator.serviceWorker ||
+        !navigator.serviceWorker.controller
+      );
+    },
+  },
+};
+</script>

+ 11 - 9
src/features/OnlineExam/Examing/QuestionBody.vue

@@ -12,20 +12,18 @@
       class="audio-div"
     >
       <div style="position: relative;" class="audio-div">
-        <audio
-          controls
-          preload="auto"
-          controlsList="nodownload"
-          :key="src"
+        <QuestionAudio
+          key="src"
           :name="name"
           :src="src"
           @play="$event => played(index, $event)"
           @ended="() => audioEnded(name)"
           @click="$event => audioClicked(index, $event)"
-        ></audio>
-        <span v-if="examQuestion.limitedPlayTimes"
-          >(剩余播放次数:{{ getAudioPlayedTimes(name) }})</span
-        ><br />
+        />
+        <span v-if="examQuestion.limitedPlayTimes">
+          (剩余播放次数:{{ getAudioPlayedTimes(name) }})
+        </span>
+        <br />
         <div
           v-if="audioInPlay.has(name)"
           style="position: absolute;top: 0;right: 0;bottom: 0;left: 0;"
@@ -38,6 +36,7 @@
 </template>
 
 <script>
+import QuestionAudio from "./QuestionAudio";
 import { createNamespacedHelpers } from "vuex";
 const { mapState, mapMutations } = createNamespacedHelpers("examingHomeModule");
 
@@ -251,6 +250,9 @@ export default {
       this.parseQuestion();
     },
   },
+  components: {
+    QuestionAudio,
+  },
 };
 </script>