{"id":14960280,"url":"https://github.com/aminwhat/unity-webgl-plugin","last_synced_at":"2026-02-22T19:33:42.139Z","repository":{"id":254622260,"uuid":"847078046","full_name":"aminwhat/Unity-WebGL-Plugin","owner":"aminwhat","description":"A Unity WebGL Plugin Sample that record audio, and send recorded audio and picked video file","archived":false,"fork":false,"pushed_at":"2024-08-24T19:41:26.000Z","size":4,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-31T03:33:12.874Z","etag":null,"topics":["jslib","plugin","unity","webgl"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aminwhat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-24T19:31:12.000Z","updated_at":"2025-01-15T09:07:12.000Z","dependencies_parsed_at":"2024-08-24T20:52:13.588Z","dependency_job_id":null,"html_url":"https://github.com/aminwhat/Unity-WebGL-Plugin","commit_stats":null,"previous_names":["aminwhat/webgl-plugin"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aminwhat%2FUnity-WebGL-Plugin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aminwhat%2FUnity-WebGL-Plugin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aminwhat%2FUnity-WebGL-Plugin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aminwhat%2FUnity-WebGL-Plugin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aminwhat","download_url":"https://codeload.github.com/aminwhat/Unity-WebGL-Plugin/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238016218,"owners_count":19402532,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["jslib","plugin","unity","webgl"],"created_at":"2024-09-24T13:21:55.672Z","updated_at":"2025-10-24T18:30:24.374Z","avatar_url":"https://github.com/aminwhat.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Unity WebGL Microphone, Video, and File Upload Plugin\n\nThis repository contains a **Unity WebGL JavaScript plugin** (a `.jslib` file) that exposes browser multimedia and file APIs to Unity via the WebGL plugin interface.  \nIt enables:\n\n- Microphone access \u0026 audio recording  \n- Audio playback \u0026 caching  \n- Video file selection \u0026 upload  \n- Chunked uploads for large audio/video files  \n- Progress tracking for text, audio, and video submissions  \n- Clearing caches between questions or sessions  \n\n\u003e The JavaScript side is designed to be paired with Unity C# calls using `[DllImport(\"__Internal\")]`.  \n\u003e Save the `.jslib` code below as `Assets/Plugins/WebGL/MyWebGLPlugin.jslib` in your Unity project.\n\n---\n\n## Features\n\n- 🎤 **Microphone Support** — Request permission, start/stop recording, check if audio is saved  \n- ▶️ **Playback Controls** — Play / pause / resume recorded audio  \n- 📼 **Video Selection** — Use browser’s file picker to select a video file (uses `showOpenFilePicker`)  \n- 📤 **Chunked Uploads** — Upload audio/video in 100 KB chunks to a server endpoint to avoid memory issues  \n- 📊 **Upload Progress \u0026 Modes** — Poll for progress value and progress mode (e.g. `voice`, `video`, `done`)  \n- 🧹 **Cache Clearing** — Clear audio/video caches between submissions or questions  \n\n---\n\n## Full JavaScript plugin (copy this file)\n\nSave this text exactly as `Assets/Plugins/WebGL/MyWebGLPlugin.jslib`:\n\n```javascript\n/**\n    [DllImport(\"__Internal\")]\n    public static extern void Initialize(string mode,string applicationName);\n\n    [DllImport(\"__Internal\")]\n    public static extern void MicroPhonePremission();\n\n    [DllImport(\"__Internal\")]\n    public static extern bool IsMicroPhonePremission();\n\n    [DllImport(\"__Internal\")]\n    public static extern void StartRecording();\n\n    [DllImport(\"__Internal\")]\n    public static extern bool IsStartRecording();\n\n    [DllImport(\"__Internal\")]\n    public static extern void StopRecording();\n\n    [DllImport(\"__Internal\")]\n    public static extern bool IsVoiceStopped();\n\n    [DllImport(\"__Internal\")]\n    public static extern bool IsVoiceSaved();\n\n    [DllImport(\"__Internal\")]\n    public static extern void PlaySavedVoice();\n\n    [DllImport(\"__Internal\")]\n    public static extern void PauseSavedVoice();\n\n    [DllImport(\"__Internal\")]\n    public static extern void ResumeSavedVoice();\n\n    [DllImport(\"__Internal\")]\n    public static extern void SelectVideoFile();\n\n    [DllImport(\"__Internal\")]\n    public static extern bool IsVideoExists();\n\n    [DllImport(\"__Internal\")]\n    public static extern string getVideoInfoVideoName();\n\n    [DllImport(\"__Internal\")]\n    public static extern string getVideoInfoVideoSize();\n\n    [DllImport(\"__Internal\")]\n    public static extern void Submit(string userId,int questionIndex,string text);\n\n    [DllImport(\"__Internal\")]\n    public static extern string getProgress();\n\n    // Available Options(by order or - default is: disable - finished is: done): [disable,text,text_failed,voice_prepare,voice,voice_failed,video_prepare,video,video_failed,done]\n    [DllImport(\"__Internal\")]\n    public static extern string getProgressMode();\n\n    [DllImport(\"__Internal\")]\n    public static extern void ClearAudioCache();\n\n    [DllImport(\"__Internal\")]\n    public static extern void ClearVideoCache();\n\n    // For Moving to the Next Question\n    [DllImport(\"__Internal\")]\n    public static extern void ClearCache();\n */\nmergeInto(LibraryManager.library, {\n  Initialize: function (mode, applicationName) {\n    const theMode = UTF8ToString(mode);\n    const theApplicationName = UTF8ToString(applicationName);\n\n    switch (theMode) {\n      case \"localhost\":\n        this.baseUrl = \"http://localhost:3000/api\";\n        break;\n      case \"ip\":\n        this.baseUrl = \"http://192.168.0.184:3000/api\";\n        break;\n      case \"production\":\n        this.baseUrl = \"/api\";\n        break;\n      default:\n        this.baseUrl = \"/api\";\n        break;\n    }\n\n    switch (theApplicationName) {\n      case \"karma\":\n        this.baseUrl = this.baseUrl + \"/karma\";\n        break;\n      case \"sazgar\":\n        this.baseUrl = this.baseUrl + \"/sazgar\";\n        break;\n      case \"aryan\":\n        this.baseUrl = this.baseUrl + \"/aryan\";\n        break;\n      default:\n        this.baseUrl = this.baseUrl + \"/karma\";\n        break;\n    }\n\n    window.addEventListener(\"beforeunload\", (event) =\u003e {\n      // Cancel the event as needed\n      event.preventDefault();\n      event.returnValue = \"\";\n    });\n  },\n\n  /**\n   * @returns {Promise\u003cvoid\u003e}\n   */\n  MicroPhonePremission: async function () {\n    try {\n      this.stream = await navigator.mediaDevices.getUserMedia({\n        audio: true,\n      });\n    } catch (err) {\n      console.error(`you got an error: ${err}`);\n    }\n  },\n\n  /**\n   * @returns {boolean}\n   */\n  IsMicroPhonePremission: function () {\n    if (this.stream) {\n      return true;\n    } else {\n      return false;\n    }\n  },\n\n  StartRecording: function () {\n    console.log(\"StartRecording \" + this.audioContext);\n    if (!this.audioContext) {\n      this.audioContext = new (window.AudioContext ||\n        window.webkitAudioContext)();\n    }\n    navigator.mediaDevices\n      .getUserMedia({ audio: true })\n      .then((stream) =\u003e {\n        this.stream = stream;\n        this.mediaRecorder = new MediaRecorder(this.stream);\n        this.mediaRecorder.ondataavailable = (event) =\u003e {\n          if (!this.audioChunks) {\n            this.audioChunks = [];\n          }\n\n          this.audioChunks.push(event.data);\n        };\n        this.mediaRecorder.start();\n        console.log(\"accessing microphone: \");\n      })\n      .catch((error) =\u003e {\n        console.error(\"Error accessing microphone: \", error);\n      });\n  },\n\n  /**\n   * @returns {boolean}\n   */\n  IsStartRecording: function () {\n    if (this.stream.getAudioTracks().length \u003e 0) {\n      return true;\n    } else {\n      return false;\n    }\n  },\n\n  StopRecording: function () {\n    this.stream.getAudioTracks().forEach((track) =\u003e {\n      track.stop();\n    });\n    this.mediaRecorder.stop();\n    this.mediaRecorder.onstop = () =\u003e {\n      console.log(\"Stop recording by microphone: \");\n      // You can also send the audioBlob to a server here.\n      var audioBlob = new Blob(this.audioChunks, {\n        type: \"audio/ogg; codecs=opus\",\n      });\n      this.audioUrl = URL.createObjectURL(audioBlob);\n      this.isVoiceStopped = true;\n    };\n  },\n\n  IsVoiceStopped: function () {\n    if (this.isVoiceStopped === null || this.isVoiceStopped === undefined) {\n      return false;\n    }\n    return this.isVoiceStopped;\n  },\n\n  /**\n   * @returns {boolean}\n   */\n  IsVoiceSaved: function () {\n    if (this.audioChunks) {\n      return true;\n    } else {\n      return false;\n    }\n  },\n\n  /**\n   * @returns {void}\n   */\n  PlaySavedVoice: function () {\n    console.log({ audioUrl: this.audioUrl });\n    this.audio = new Audio(this.audioUrl);\n    this.audio.controls = true;\n    this.audio.play();\n  },\n\n  PauseSavedVoice: function () {\n    if (this.audio) {\n      this.audio.pause();\n    }\n  },\n\n  ResumeSavedVoice: function () {\n    if (this.audio \u0026\u0026 this.audio.paused) {\n      this.audio.play();\n    }\n  },\n\n  /**\n   * @returns {void}\n   */\n  SelectVideoFile: async function () {\n    if (!window.showOpenFilePicker) {\n      window.alert(\"این قابلیت در این مرورگر پشتیبانی نمی شود\");\n    } else {\n      /**\n       * @type {FileSystemFileHandle}\n       */\n      const fileHandles = await window.showOpenFilePicker({\n        multiple: false,\n        excludeAcceptAllOption: true,\n        types: [\n          {\n            description: \"Videos/ویدئو\",\n            accept: { \"video/*\": [\".mp4\", \".mkv\", \".avi\", \".mov\"] },\n          },\n        ],\n      });\n\n      this.videoFileName = fileHandles[0].name;\n      /**\n       * @type {File}\n       */\n      this.videoFile = await fileHandles[0].getFile();\n    }\n  },\n\n  /**\n   * @returns {boolean}\n   */\n  IsVideoExists: function () {\n    if (this.videoFileName) {\n      return true;\n    } else {\n      return false;\n    }\n  },\n\n  /**\n   * @returns {string}\n   */\n  getVideoInfoVideoName: function () {\n    //Allocate memory space\n    var buffer = _malloc(lengthBytesUTF8(this.videoFileName) + 1);\n    //Copy old data to the new one then return it\n    writeStringToMemory(this.videoFileName, buffer);\n    return buffer;\n  },\n\n  /**\n   * @returns {string}\n   */\n  getVideoInfoVideoSize: function () {\n    var buffer = _malloc(lengthBytesUTF8(String(this.videoFile.size)) + 1);\n\n    writeStringToMemory(String(this.videoFile.size), buffer);\n\n    return buffer;\n  },\n\n  /**\n   * @returns {void}\n   */\n  Submit: async function (userId, questionIndex, text) {\n    const theUserId = UTF8ToString(userId);\n    const theText = UTF8ToString(text);\n\n    this.progress = 0;\n    this.progressMode = \"disable\";\n\n    if (\n      theText \u0026\u0026\n      (this.isTextSent === undefined ||\n        this.isTextSent === null ||\n        !this.isTextSent)\n    ) {\n      console.log(\"Sending Text\");\n      const body = {};\n\n      this.progressMode = \"text\";\n      this.progress = 10;\n      body.userId = theUserId;\n      body.questionIndex = questionIndex;\n      body.text = theText;\n\n      console.log({ reqUrl: this.baseUrl + \"/manager\", body });\n\n      try {\n        const response = await fetch(this.baseUrl + \"/manager\", {\n          signal: AbortSignal.timeout(300000),\n          method: \"POST\",\n          mode: \"cors\",\n          credentials: \"same-origin\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n            \"connect-src\": \"self\",\n          },\n          body: JSON.stringify(body),\n        });\n\n        console.log(\"Text sent to server\");\n\n        this.isTextSent = response.ok;\n\n        const data = await response.json();\n        console.log(\"Response: \", JSON.stringify(data));\n        this.progress = 100;\n      } catch (e) {\n        this.progressMode = \"text_failed\";\n        console.error(\"Error sending text to server: \", error);\n        this.isTextSent = false;\n        return;\n      }\n    } else {\n      this.isTextSent = true;\n    }\n\n    if (\n      this.audioChunks \u0026\u0026\n      this.audioChunks !== undefined \u0026\u0026\n      (this.isVoiceSent === undefined ||\n        this.isVoiceSent === null ||\n        !this.isVoiceSent)\n    ) {\n      this.progressMode = \"voice_prepare\";\n      var audioBlob = new Blob(this.audioChunks, {\n        type: \"audio/wav\",\n      });\n      const arrayBuffer = await new Response(audioBlob).arrayBuffer();\n\n      const voiceArray = new Uint8Array(arrayBuffer);\n\n      this.progressMode = \"voice\";\n      this.progress = 0;\n      for (let i = 0; i \u003c voiceArray.length; i += 102400) {\n        this.progress = Math.floor((i / voiceArray.length) * 100);\n        console.log({ progress: this.progress });\n        const chunk = voiceArray.slice(i, i + 102400);\n        try {\n          await fetch(\n            this.baseUrl + \"/voice/upload/\" + theUserId + \"/\" + questionIndex,\n            {\n              signal: AbortSignal.timeout(900000),\n              method: \"POST\",\n              mode: \"cors\",\n              credentials: \"same-origin\",\n              headers: {\n                \"Content-Type\": \"application/octet-stream\",\n              },\n              body: chunk,\n            }\n          );\n        } catch (e) {\n          this.progressMode = \"voice_failed\";\n          console.error(\"Error sending voice Chunks to server: \", error);\n          this.isVoiceSent = false;\n          break;\n        }\n      }\n      if (this.progressMode === \"voice_failed\") {\n        return;\n      }\n\n      const body = {};\n      body.userId = theUserId;\n      body.questionIndex = questionIndex;\n      console.log({ body });\n\n      try {\n        const response = await fetch(this.baseUrl + \"/voice/submit\", {\n          signal: AbortSignal.timeout(900000),\n          method: \"POST\",\n          mode: \"cors\",\n          credentials: \"same-origin\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n            \"connect-src\": \"self\",\n          },\n          body: JSON.stringify(body),\n        }).catch((error) =\u003e {\n          console.error(\"Error sending voice to server: \" + error);\n          this.isVoiceSent = true;\n        });\n\n        console.log(\"voice Sent to the Server\");\n\n        this.isVoiceSent = response.ok;\n        const data = await response.json();\n        console.log(\"Response: \", JSON.stringify(data));\n        this.progress = 100;\n      } catch (error) {\n        this.progressMode = \"voice_failed\";\n        console.error(\"Error sending voice to server: \", error);\n        this.isVoiceSent = false;\n        return;\n      }\n    } else {\n      this.isVoiceSent = true;\n    }\n\n    if (\n      this.videoFile \u0026\u0026\n      this.videoFileName \u0026\u0026\n      (this.isVideoSent === undefined ||\n        this.isVideoSent === null ||\n        !this.isVideoSent)\n    ) {\n      this.progressMode = \"video_prepare\";\n      // Convert Blob to ArrayBuffer\n      const arrayBuffer = await new Response(this.videoFile).arrayBuffer();\n\n      // Convert ArrayBuffer to Uint8Array (if needed)\n      const videoArray = new Uint8Array(arrayBuffer);\n\n      this.progressMode = \"video\";\n      this.progress = 0;\n      for (let i = 0; i \u003c videoArray.length; i += 102400) {\n        this.progress = Math.floor((i / videoArray.length) * 100);\n        console.log({ progress: this.progress });\n        const chunk = videoArray.slice(i, i + 102400);\n        try {\n          await fetch(\n            this.baseUrl + \"/video/upload/\" + theUserId + \"/\" + questionIndex,\n            {\n              signal: AbortSignal.timeout(900000),\n              method: \"POST\",\n              mode: \"cors\",\n              credentials: \"same-origin\",\n              headers: {\n                \"Content-Type\": \"application/octet-stream\",\n              },\n              body: chunk,\n            }\n          );\n        } catch (e) {\n          this.progressMode = \"video_failed\";\n          console.error(\"Error sending video Chunks to server: \", error);\n          this.isVideoSent = false;\n          break;\n        }\n      }\n      if (this.progressMode === \"video_failed\") {\n        return;\n      }\n\n      const body = {};\n      body.userId = theUserId;\n      body.questionIndex = questionIndex;\n      body.fileType = String(this.videoFileName).split(\".\").pop();\n      console.log({ body });\n\n      try {\n        const response = await fetch(this.baseUrl + \"/video/submit\", {\n          signal: AbortSignal.timeout(900000),\n          method: \"POST\",\n          mode: \"cors\",\n          credentials: \"same-origin\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n            \"connect-src\": \"self\",\n          },\n          body: JSON.stringify(body),\n        }).catch((error) =\u003e {\n          console.error(\"Error sending video to server: \" + error);\n          this.isVideoSent = true;\n        });\n\n        console.log(\"video Sent to the Server\");\n\n        this.isVideoSent = response.ok;\n        const data = await response.json();\n        console.log(\"Response: \", JSON.stringify(data));\n        this.progress = 100;\n      } catch (error) {\n        this.progressMode = \"video_failed\";\n        console.error(\"Error sending video to server: \", error);\n        this.isVideoSent = false;\n        return;\n      }\n    } else {\n      this.isVideoSent = true;\n    }\n\n    if (this.isTextSent \u0026\u0026 this.isVoiceSent \u0026\u0026 this.isVideoSent) {\n      this.progress = 100;\n      this.progressMode = \"done\";\n    }\n  },\n\n  /**\n   * @returns {number}\n   */\n  getProgress: function () {\n    if (this.progress === null || this.progress === undefined) {\n      return 0;\n    }\n\n    var buffer = _malloc(lengthBytesUTF8(String(progress)) + 1);\n    writeStringToMemory(String(progress), buffer);\n    return buffer;\n  },\n\n  /**\n   * @returns {string}\n   */\n  getProgressMode: function () {\n    let progressMode = this.progressMode;\n    if (progressMode === null || progressMode === undefined) {\n      progressMode = \"disable\";\n    }\n\n    var buffer = _malloc(lengthBytesUTF8(String(progressMode)) + 1);\n    writeStringToMemory(String(progressMode), buffer);\n    return buffer;\n  },\n\n  /**\n   * @returns {void}\n   */\n  ClearAudioCache: function () {\n    this.recorder = null;\n    this.mediaRecorder = null;\n    this.audioContext = null;\n    this.audioChunks = null;\n    this.audio = null;\n    this.stream = null;\n    this.isVoiceSent = null;\n    this.isVoiceStopped = null;\n  },\n\n  /**\n   * @returns {void}\n   */\n  ClearVideoCache: function () {\n    this.videoFile = null;\n    this.videoFileName = null;\n    this.isVideoSent = null;\n  },\n\n  /**\n   * @returns {void}\n   */\n  ClearCache: function () {\n    this.recorder = null;\n    this.mediaRecorder = null;\n    this.audioContext = null;\n    this.audioChunks = null;\n    this.audio = null;\n    this.stream = null;\n    this.videoFile = null;\n    this.videoFileName = null;\n    this.isVideoSent = null;\n    this.isTextSent = null;\n    this.isVoiceSent = null;\n    this.isVoiceStopped = null;\n  },\n});\n```\n\n---\n\n## Unity C# extern declarations\n\nAdd these in a C# file where you call into the plugin (e.g. `WebGLPluginBridge.cs`):\n\n```csharp\nusing System.Runtime.InteropServices;\n\npublic static class WebGLPluginBridge\n{\n    [DllImport(\"__Internal\")] public static extern void Initialize(string mode, string applicationName);\n    [DllImport(\"__Internal\")] public static extern void MicroPhonePremission();\n    [DllImport(\"__Internal\")] public static extern bool IsMicroPhonePremission();\n    [DllImport(\"__Internal\")] public static extern void StartRecording();\n    [DllImport(\"__Internal\")] public static extern bool IsStartRecording();\n    [DllImport(\"__Internal\")] public static extern void StopRecording();\n    [DllImport(\"__Internal\")] public static extern bool IsVoiceStopped();\n    [DllImport(\"__Internal\")] public static extern bool IsVoiceSaved();\n    [DllImport(\"__Internal\")] public static extern void PlaySavedVoice();\n    [DllImport(\"__Internal\")] public static extern void PauseSavedVoice();\n    [DllImport(\"__Internal\")] public static extern void ResumeSavedVoice();\n    [DllImport(\"__Internal\")] public static extern void SelectVideoFile();\n    [DllImport(\"__Internal\")] public static extern bool IsVideoExists();\n    [DllImport(\"__Internal\")] public static extern string getVideoInfoVideoName();\n    [DllImport(\"__Internal\")] public static extern string getVideoInfoVideoSize();\n    [DllImport(\"__Internal\")] public static extern void Submit(string userId, int questionIndex, string text);\n    [DllImport(\"__Internal\")] public static extern string getProgress();\n    [DllImport(\"__Internal\")] public static extern string getProgressMode();\n    [DllImport(\"__Internal\")] public static extern void ClearAudioCache();\n    [DllImport(\"__Internal\")] public static extern void ClearVideoCache();\n    [DllImport(\"__Internal\")] public static extern void ClearCache();\n}\n```\n\n\u003e **Note**: In some Unity/WebGL setups you may prefer to declare the return type of `getProgress` / `getProgressMode` as `IntPtr` and then use `Marshal.PtrToStringAuto()` to convert the returned pointer to a managed string. However Unity's IL2CPP/WebGL will often marshal string results as shown above — test in your build.\n\n---\n\n## Example Unity usage (MonoBehaviour)\n\n```csharp\nusing UnityEngine;\n\npublic class ExampleUsage : MonoBehaviour\n{\n    void Start()\n    {\n        // Initialize environment and application name (\"karma\" / \"sazgar\" / \"aryan\")\n        WebGLPluginBridge.Initialize(\"production\", \"karma\");\n\n        // Request microphone permission (prompts user)\n        WebGLPluginBridge.MicroPhonePremission();\n    }\n\n    void Update()\n    {\n        // Poll permission and progress to update UI\n        if (WebGLPluginBridge.IsMicroPhonePremission())\n        {\n            // allow the record button\n        }\n\n        // Example of reading progress mode and progress value:\n        string mode = WebGLPluginBridge.getProgressMode();\n        string progress = WebGLPluginBridge.getProgress(); // returns stringified number (0-100)\n    }\n\n    public void OnStartRecordPressed()\n    {\n        WebGLPluginBridge.StartRecording();\n    }\n\n    public void OnStopRecordPressed()\n    {\n        WebGLPluginBridge.StopRecording();\n    }\n\n    public void OnPlayRecorded()\n    {\n        WebGLPluginBridge.PlaySavedVoice();\n    }\n\n    public void OnSelectVideo()\n    {\n        WebGLPluginBridge.SelectVideoFile();\n    }\n\n    public void OnSubmit(string userId, int questionIndex, string text)\n    {\n        WebGLPluginBridge.Submit(userId, questionIndex, text);\n    }\n}\n```\n\n---\n\n## Server endpoints expected (plugin-side)\n\nThe plugin expects the following endpoints under `baseUrl`:\n\n- `POST /manager` — receives `{ userId, questionIndex, text }` (JSON)  \n- Multiple `POST /voice/upload/:userId/:questionIndex` — binary chunks of voice (100 KB)  \n- `POST /voice/submit` — final voice submit metadata `{ userId, questionIndex }`  \n- Multiple `POST /video/upload/:userId/:questionIndex` — binary chunks of video (100 KB)  \n- `POST /video/submit` — final video submit metadata `{ userId, questionIndex, fileType }`\n\nAdjust your server-side to accept multiple chunk uploads and reassemble them.\n\n---\n\n## Known issues / tips\n\n- **Bug in `getProgress()`** — The plugin code uses `String(progress)` and `lengthBytesUTF8(String(progress))` but `progress` is not a defined local variable there. It should likely use `this.progress`. If you see `0` or unexpected values, fix the function to reference `this.progress`. Example fix:\n\n```javascript\ngetProgress: function () {\n  if (this.progress === null || this.progress === undefined) {\n    return 0;\n  }\n  var buffer = _malloc(lengthBytesUTF8(String(this.progress)) + 1);\n  writeStringToMemory(String(this.progress), buffer);\n  return buffer;\n},\n```\n\n- **`showOpenFilePicker` support** — Not all browsers support the modern file picker. Fallbacks (e.g., an `\u003cinput type=\"file\"\u003e`) can be implemented if you need wider browser support.\n- **HTTPS** — Microphone access requires a secure context (HTTPS) except when using `localhost` during development.\n- **Memory / chunk sizes** — The code chunks by `102400` (100 KB); you can change this size to tune upload performance vs. memory usage.\n- **Error handling** — The plugin logs errors to the browser console. Add more robust retry or user-facing error messages in production.\n- **Large files \u0026 timeouts** — The fetch calls use `AbortSignal.timeout(900000)` (15 minutes) to allow large uploads. Tune according to your server/network.\n\n---\n\n## Browser Compatibility\n\n- Microphone: works on modern browsers when served over HTTPS (Chrome, Edge, Firefox).  \n- File picker: `showOpenFilePicker` is currently best supported in Chromium-based browsers. For others, the plugin will alert the user in Persian (\"این قابلیت در این مرورگر پشتیبانی نمی شود\").\n\n---\n\n## Troubleshooting\n\n- If recorded audio won't play, open DevTools console to see errors and confirm `this.audioUrl` exists.  \n- If `getVideoInfoVideoName` or `getVideoInfoVideoSize` return empty, ensure `SelectVideoFile()` was called and the user picked a file.  \n- On mobile browsers, MediaRecorder or showOpenFilePicker may not be available — consider mobile-specific fallbacks.\n\n---\n\n## Contributing\n\nContributions welcome. If you:\n1. Find bugs (e.g. `getProgress()` bug), please open an issue and submit a PR.  \n2. Want to add features (e.g., file picker fallback, retry logic, progress callbacks to Unity), open an issue to discuss before implementing.\n\n---\n\n## License\n\nMIT License — copy, modify, and distribute. Attribution appreciated but not required.\n\n---\n\n## Contact\n\nIf you'd like, I can also:\n- Create a small Unity demo scene that integrates this plugin and demonstrates recording, picking a video, and submitting.\n- Create a GitHub Actions workflow to run a static check or build a test WebGL build.\n\nTell me which and I’ll add it to the repo.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faminwhat%2Funity-webgl-plugin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faminwhat%2Funity-webgl-plugin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faminwhat%2Funity-webgl-plugin/lists"}