顯示具有 WebAPIs 標籤的文章。 顯示所有文章
顯示具有 WebAPIs 標籤的文章。 顯示所有文章

2019年12月6日

[WebAPIs] Picture In Picture of Video

keywords: video, WebAPIs

TL;DR

新技術一定有風險,瀏覽器有支援有不支援(目前只有 Chrome 預設是支援的),使用前應詳閱公開說明書!
// 檢驗瀏覽器有無支援 Picture In Picture API
if ('pictureInPictureEnabled' in document) {
  // 有支援...
}

// 進入 video 的 PIP 模式
videoElement.requestPictureInPicture().catch((error) => {
  // 錯誤處理...
});

// 離開 video 的 PIP 模式
document.exitPictureInPicture().catch((error) => {
  // Error handling
});
這篇文章內容主要整理自 An Introduction to the Picture-in-Picture Web API @ CSS Tricks

Picture In Picture 是什麼?

Video 元素的 Picture in Picture 模式可以讓影片獨立出來播放(下圖上方),甚至可以在其他頁籤繼續觀看原本的影片(下圖下方):
Imgur

進入和離開 Picture In Picture

// 進入 Picture In Picture
videoElement.requestPictureInPicture().catch((error) => {
  // Error Handling
});

// 離開 Picture In Picture
document.exitPictureInPicture().catch((error) => {
  // Error Handling
});

事件(Events)

videoElement.addEventListener('enterpictureinpicture', () => {
  notice.textContent = 'Enter Picture-in-Picture mode';
});

videoElement.addEventListener('leavepictureinpicture', () => {
  notice.textContent = 'Exit Picture-in-Picture mode';
});

在 PIP 的視窗上測做或增加功能鍵

只要透過 navigator.mediaSession.setActionHandler 就可以在 PIP 的視窗上操作不同的功能鍵:
// 當使用者點擊特定操作鍵時
navigator.mediaSession.setActionHandler('play', function() {
  // User clicked "Play" button.
});
navigator.mediaSession.setActionHandler('pause', function() {
  // User clicked "Pause" button.
});

同時也可以增加原本上面沒有顯示的功能鍵:
// 在 PIP 的視窗上增加前一部、後一部的功能鍵
navigator.mediaSession.setActionHandler('previoustrack', () => {
  // Go to previous track
});

navigator.mediaSession.setActionHandler('nexttrack', () => {
  // Go to next track
});
Imgur

在使用者前鏡頭的畫面呈現於 PIP 上

同樣地,也可以將裝置前置鏡頭的畫面呈現於 PIP 的視窗上,程式碼的部分可以參考 CSS Tricks 上的這個 CodePen:
## 避免瀏覽器使用 PIP 功能
在 HTML 的 <video> 標籤中加入 disablePictureInPicture 即可:
<video disablePictureInPicture controls src="video.mp4>"></video>

程式範例

參考資料

2019年11月20日

[WebAPIs] Web Share API 的使用 - navigator.share

keywords: WebAPIs, mobile, navigator.share

TL;DR

// 判斷瀏覽器是否支援 Web Share API
if (navigator.share) {
  // navigator.share 會回傳 Promise
  navigator.share({
    title: 'Web Fundamentals',
    text: 'Check out Web Fundamentals — it rocks!',
    url: 'https://developers.google.com/web',
  })
  .then(() => console.log('Successful share'))
  .catch((error) => console.log('Error sharing', error));
}

Web Share API 的說明

用手機瀏覽網頁的時候,不知道你有沒有注意過,當你按下分享按鈕時它會跳出一個選單,讓你可以選擇要分享到哪個 App 的選單,像是這樣:
Web Share API
這個功能過去需要透過點擊手機瀏覽器上的「分享」後才會出現:
Web Share API
但現在透過 Web Share 這個 API 也可以輕鬆達到這個功能,讓使用者在點擊網頁上的按鈕後就跳出這個「分享選單」,如此就有機會省去使用那種帶有一堆 Facebook、Line、Twitter、Pinterest 按鈕的第三方套件,進一步減少頁面載入時間。
現在就來看看怎麼使用吧!

Web Share API - navigator.share

適用瀏覽器

Web Share API 的使用方式很簡單,但要注意的是這主要是適用在手機上的功能,畢竟電腦上沒有這種分享選單(目前除了 Mac 的 Safari 可用),不過即使電腦不支援此 API 的使用,還是可以很容易做出替代方案(fallback)。
看到下圖瀏覽器支援性的一大片紅字感覺很恐怖,但目前(2019-11-20)其實主要的手機瀏覽器(Chrome, Safari)都適用,Mac 上的 Safari 亦可:
Web Share API
目前實測在三星內建的瀏覽器上可以使用,但分享成功後的回傳訊息不太正確。

使用方式

使用方式很簡單:
// navigator.share 會回傳一個 Promise

// 下面的欄位可以不用全部填寫,可以只分享網址,也可以只分享文字
const sharePromise = navigator.share({
  url: 'https://pjchender.blogspot.com',    // 要分享的 URL
  title: 'PJCHENder 那些沒告訴你的小細節',      // 要分享的標題
  text: '好多眉眉角角啊'             // 要分享的文字內容
});
navigator.share() 會回傳一個 Promise (代表你可以搭配 async...await 使用),這個 Promise 會在使用者完成點擊某個 App 分享後被完成(fulfilled);若使用者取消分享或帶入的參數有錯誤時,則會被拒絕(reject)。
使用時有幾點需留意一下:
  1. Web Share API 只能使用在有 HTTPS 的網站或者是測試時的 localhost,若想玩玩看這個 API 就可以到帶有 HTTPS 的 CodePen 上試試看。
  2. 需要透過使用者主動的行爲(user activation)才能觸發,例如,點擊事件。

範例程式碼

來看一下範例程式碼吧,也可以直接看 CodePen
See the Pen Web Share API by PJCHEN (@PJCHENder) on CodePen.

HTML

先建立最基本的一個按鈕:
<div class="center-center">
  <button>Share<i class="fas fa-share-alt"></i></button>
  <p class="result"></p>
</div>

JavaScript

  1. 透過 document.querySelector() 選擇和 DOM 有關的元素
  2. 建立使用者點擊分享時要帶入的資訊,不用每一項都填寫,也可以只分享文字或網址
  3. Web Share API 需要使用者主動的行為才能觸發, 所以透過 addEventListener 監聽使用者點擊 click 事件
  4. 透過 navigator.share 來使用 Web Share API
  5. 當使用者拒絕分享或發生錯誤時要顯示的訊息
// STEP 1:選擇和 DOM 有關的元素
const btn = document.querySelector('button');
const result = document.querySelector('.result');

// STEP 2:建立使用者點擊分享時要帶入的資訊
const shareData = {
  url: 'https://pjchender.blogspot.com', // 要分享的 URL
  title: 'PJCHENder 那些沒告訴你的小細節', // 要分享的標題
  text: '好多眉眉角角啊', // 要分享的文字內容
};

// STEP 3:當使用者點擊按鈕時
btn.addEventListener('click', async () => {
  try {
    // STEP 4:使用 Web Share API
    await navigator.share(shareData);
    result.textContent = '感謝你的的分享';
  } catch (err) {
    // STEP 5:使用者拒絕分享或發生錯誤
    const { name, message } = err;
    if (name === 'AbortError') {
      result.textContent = '您已取消分享此訊息';
    } else {
      result.textContent = err;
      console.log('發生錯誤', err);
    }
  }
});
如此就完成這個簡單的範例了。
這個範例的按鈕樣式是修改自 AyooluwaCodePen
來看一下實作的結果:
Web Share API
以 Line 為例,傳送出去的訊息內容如下:
Web Share API
See the Pen Web Share API by PJCHEN (@PJCHENder) on CodePen.

替代處理與其他(fallback)

對於不支援 Web Share API 的瀏覽器則可以透過判斷 navigator.share 是否存在來進行替代方案:
if (navigator.share) {
  // 使用 Web Share API
} else {
  // 替代方案寫在這...
}
舉例來說,在 CSS Tricks 的 How to Use the Web Share API 文章中提供了非常精緻的替代處理畫面:
Imgur
可以參考這個作者的 CodePen 範例。
如果不想這麼複雜的話,替代方案也可以是點擊按鈕後複製網址(Copy Link)給使用者自行分享即可。複製到剪貼簿的方式同樣有對應的 WebAPIs 可以支援,有需要可以參考先前整理的筆記 [WebAPIs] Copy to clipboard 複製到剪貼簿

範例程式碼:當瀏覽器不支援時讓按鈕變成複製功能

這裡提供實際的範例可以作為參考:
// 選擇和 DOM 有關的元素
const btn = document.querySelector('button');
const result = document.querySelector('.result');

// 當使用者點擊分享時要帶入的資訊
const shareData = {
  url: 'https://pjchender.blogspot.com', // 要分享的 URL
  title: 'PJCHENder 那些沒告訴你的小細節', // 要分享的標題
  text: '好多眉眉角角啊', // 要分享的文字內容
};

btn.addEventListener('click', () => {
  // 判斷瀏覽器是否支援 Web Share API
  if (navigator.share) {
    handleNavigatorShare();
  } else {
    handleNotSupportNavigatorShare();
  }
});

// 當瀏覽器支援 Web Share API 時
async function handleNavigatorShare() {
  try {
    await navigator.share(shareData);
    result.textContent = '感謝你的的分享';
  } catch (err) {
    // 使用者拒絕分享或發生錯誤
    const { name } = err;
    if (name === 'AbortError') {
      result.textContent = '您已取消分享此訊息';
    } else {
      result.textContent = err;
      console.log('發生錯誤', err);
    }
  }
}

// 當瀏覽器不支援 Web Share API 時,點下去變成複製
function handleNotSupportNavigatorShare() {
  const contentToCopy = document.querySelector('#content-to-copy');
  contentToCopy.value = shareData.url;
  contentToCopy.setAttribute('type', 'text'); // 不是 hidden 才能複製
  contentToCopy.select();

  try {
    const successful = document.execCommand('copy');
    const msg = successful ? '成功' : '失敗';
    alert(`${shareData.url} - 複製${msg}`);
  } catch (err) {
    alert('Oops, unable to copy');
  }

  /* unselect the range */
  contentToCopy.setAttribute('type', 'hidden');
  window.getSelection().removeAllRanges();
}

參考文章

2019年7月2日

[筆記] WebRTC 網路影音 -實作篇(demo of media, video, audio)

Photo by freestocks.org on Unsplash
以下內容完全為整理自 30天之即時網路影音開發攻略(小白本) by 我是小馬克 @ iThome 的筆記,無原創內容。

建立串流點播工具

將影音檔轉成 HLS 用的串流檔

透過 ffmpeg 即可將影音檔(例如,mp4)轉成 .m3u8.ts 適用於 HLS 的串流檔。
在 Node.js 中可以使用 fluent-ffmpeg 這個工具進行轉換:
// ./ffmpeg-helper.js
// 把 mp4 檔轉換成 .m3u8 索引檔和多支 .ts 檔
const ffmpeg = require('fluent-ffmpeg');

module.exports = {
  convertToHls: async (file) => {
    return new Promise((resolve) => {
      ffmpeg(file, { timeout: 432000 })
        .addOptions([
          '-profile:v baseline', // for H264 video codec
          '-level 3.0',
          '-s 640x360', // 640px width, 360px height
          '-start_number 0', // start the first .ts segment at index 0
          '-hls_time 10', // 10 second segment duration
          '-hls_list_size 0', // Maximum number of playlist entries
          '-f hls', // HLS format
        ])
        .output('./source-m3u8/output.m3u8')
        .on('end', () => {
          console.log('finish');
          resolve();
        })
        .run();
    });
  },
};
並且建立一支 convert.js 來執行:
// ./convert.js
const ffmpegHelper = require('./ffmpeg-helper');

(async () => {
   await ffmpegHelper.convertToHls('./source.mp4');
})();
轉換好的檔案會被放置到 source-m3u8 的資料夾內:
imgur

建立支援 HLS 的 Server

要建立 HLS 的 Node 伺服器可以使用套件 hls-server
// index.js
const HLSServer = require('hls-server');
const http = require('http');

const hls = new HLSServer(server, {
  path: '/streams', // Base URI to output HLS streams
  dir: 'source-m3u8', // Directory that input files are stored
});

server.listen(8000);

使用 ffplay 播放串流檔

$ node index.js    # 於專案資料夾下執行
$ ffplay http://localhost:8000/streams/output.m3u8
如此即可開始播放:
imgur

在瀏覽器進行播放

若要在瀏覽器進行播放,一樣需要額外的套件,這裡用的是 hls.js
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>HLS Client</title>
  <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>

<body>
  <div id="app">
    <video controls id="video"></video>

    <input type="text" />
    <button type="button" id="load">Load</button>
  </div>

  <script>
  const video = document.getElementById('video');
  const button = document.querySelector("#load");

  if (Hls.isSupported()) {
    var hls = new Hls();
    hls.attachMedia(video);
    hls.on(Hls.Events.MANIFEST_PARSED, function () {
      video.play();
    });
    button.addEventListener("click", function () {
      hls.loadSource(document.querySelector("input").value);
    })
  } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.addEventListener('canplay', function () {
      video.play();
    });
    button.addEventListener("click", function () {
      video.src = document.querySelector("input").value;
    })
  }

  </script>
</body>

</html>
Server 的部分做一些修改,讓它可以 serve 這支 index.html
// index.js
const HLSServer = require('hls-server');
const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/html');
  const html = fs.readFileSync('index.html', 'utf8');
  res.write(html);
  res.end();
});

const hls = new HLSServer(server, {
  path: '/streams', // Base URI to output HLS streams
  dir: 'source-m3u8', // Directory that input files are stored
});

server.listen(8000);
完成後,順利的話只要在表單中輸入 http://localhost:8000/streams/output.m3u8 就可以在網頁上載到影片:
imgur

建立直播工具

直播的部分會分成「推流」和「拉流」兩個部分:
imgur
  • 在 server 的部分,這裡會使用到 Node-Media-Server 這個套件來接收從直撥主送過來的推流,並提供 .flv 檔給 Client。
  • 在 client 的部分,會使用 bilibili 提供的 flv.js 來拉流。
要建立直播,首先要把 media server 給 on 起來:
$ node app.js
接著透過 ffmpeg 假裝是直播主透過 RTMP 協定進行推流:
$ ffmpeg -re -i source.mp4 -c copy -f flv rtmp://localhost/live/mark
這時候 server 就可以收到直播主的推流,client 的部分則可以使用 flv.js 來拉流:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Live Streaming</title>
    <script src="https://cdn.bootcss.com/flv.js/1.4.2/flv.min.js"></script>
  </head>

  <body>
    <video controls id="video" width="100%"></video>

    <input type="text" />
    <button type="button" id="load">Load</button>
    <script>
      const button = document.querySelector('#load');
      if (flvjs.isSupported()) {
        button.addEventListener('click', function() {
          const video = document.getElementById('video');
          const target = document.querySelector('input').value;
          const flvPlayer = flvjs.createPlayer({
            type: 'flv',
            url: `http://localhost:8000/live/${target}.flv`,
          });
          flvPlayer.attachMediaElement(video);
          flvPlayer.load();
          flvPlayer.play();
        });
      }
    </script>
  </body>
</html>

影音串流伺服器常見問題

作為一個影音串流伺服器,常見的問題包含「伺服器連線數限制」、「流量頻寬問題」、「效能消耗問題」、「遠距離封包遺失問題」。
透過 Load Balance 可以解決「伺服器連線數限制」和「效能消耗問題」;而「流量頻寬問題」和「遠距離封包遺失問題」則可以透過 CDN 解決。
沒有 CDN 的協助基本上是很難完成影音串流伺服器的。

Content Delivery Network (CDN)

透過內容交付網路(CDN, Content Delivery Network),即使伺服器架設在美國,而使用者人在台灣,依然可以用更短的時間取得這些資源。
CDN 主要可以分成三個部分:
  • 智能 DNS(Intelligent DNS):告訴 client 最近的 Edge CDN 位址在哪
  • 邊緣 CDN(Edge CDN):散佈在世界各地的 CDN 節點
  • 來源伺服器(Origin Server):原始資料存放的位址
imgur
關於如何在 AWS 上設定 CDN 可以參考 30-23之 CDN 的說話島 ( AWS CloudFront CDN 實作 ) by 我是小馬克 @ iThome

WebRTC 與 SDP

透過 WebRTC 可以讓瀏覽器不需要任何外掛的情況下直接進行 P2P 的溝通,因此這裡示範如何用 WebRTC 建立 P2P 的影音連線。
雖然 WebRTC 可以讓使用者直接透過瀏覽器進行 P2P 連線,但在雙方建立連線前仍需要有一個伺服器讓他們知道雙方的位址,才能進行溝通,這個伺服器稱作 Signaling Server,這並沒有規範在 WebRTC 內,可以使用 HTTP 輪詢(一直打 request 詢問)或 websocket 的方式實作。這裡會使用到 peer.js 這個套件來實作。
當雙方還不認識彼此時,需要透過傳送自己的 SDP( Session Description Protocol,會話描述協議) 到 Signaling Server 來認識彼此,SDP 中會包含有發送者的地址、媒體類型、傳輸協議、媒體格式。
WebRTC 加上 SDP 後的流程如下:
  1. 用戶 A 向 Server 發送進行會話,內容包含 A 的 SDP。
  2. Server 將會話 SDP A 請求發送給用戶 B。
  3. 用戶 B 向 Server 進行應答,並回應 B 的 SDP。
  4. Server 向用戶 A 發送 用戶 B 的 SDP。
  5. A、B 雙方使用 SDP 開始建立連線。
imgur

影音串流在 P2P 上的困難與解法

當透過 P2P 的方式進行影音傳輸,因此當使用者有使用「網路位址轉換(NAT, Network Address Translation)」或防火牆時,會沒辦法直接找到目標電腦的位址。
在 WebRTC 則使用了 ICE (Interactive Connectivity Establishment) 這個框架來解決這個問題,ICE 整合了 STUN (Session Traversal Utilities for NAT)TURN (Traversal Using Relay NAT ) 兩個協定,讓 NAT 內的用戶一樣可以找到它的位置。

參考資源

2019年1月16日

[JS] 透過 JavaScript 處理檔案上傳(AJAX Upload byte / JSON / formData File)

HTML Input File

使用 <input type="file" /> 取得使用者想要上傳的檔案:
  • multiple 屬性可以一次上傳多個檔案
  • accept 屬性可以限制上傳檔案的類型
<input type="file" id="file-uploader" data-target="file-uploader" accept="image/*" multiple="multiple"/>

限制可上傳的檔案類型 Accept Attribute

accept="image/png"
accept=".png"


accept="image/png, image/jpeg"
accept=".png, .jpg, .jpeg"

accept="image/*"
accept=".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"

取得上傳檔案的基本資訊

keywords: fileInput.files
透過 e.target.files 屬性可以取得該檔案的 Blob 物件
const fileUploader = document.querySelector('#file-uploader');

fileUploader.addEventListener('change', (e) => {
  console.log(e.target.files); // get file object
});
❗️ e.target.files 會是一個陣列,裡面可以取得使用者所有想要上傳的檔案,陣列裡都是該檔案的 Blob 物件,而不是一般的物件。
因為這裡只有上傳一個檔案,所以使用 e.target.files[0] 即可取得使用者想要上傳的檔案。這的 File Object 是一個 Blob 物件而不是一般的物件,但從中可以透過 name, size, type 取得該檔案的資訊。
imgur

透過 AJAX 上傳檔案

FormData

keywords: FormData()
透過下面的方式,可以將欲上傳的檔案 append 到 FormData() 上:
let form = new FormData();
form.append("product[photos][]", e.target.files[i])
接著透過 fetch API 或其他方式把檔案送到後端:
// fetchAPI
fetch('https://api.endpoint.io', {
  method: 'POST',
  body: form,
})

// jQuery
$.ajax({
  processData: false,
  data: form,
})

JSON

另一種方式是透過 JSON 來上傳檔案,步驟如下:
  1. 取得使用者上傳檔案:在 HTML 中建立 <input type="file" onChange={handleUpload} /> 來取得使用者上傳的檔案。
  2. 得到該檔案的 Blob:在 handleUploade.target.files 中可以取得該檔案的 Blob
  3. 轉成 ArrayBuffer:透過 FilerReader() 來轉成 ArrayBuffer 的格式。在 reader.onLoad 的時候,可以透過 reader.result 來取得 ArrayBuffer。
  4. 轉成 Uint8Array:接著透過 new Uint8Array() 把這個 ArrayBuffer 轉成陣列,但要特別注意,轉出來的是「類陣列(TypedArray)」而不是真正的陣列,因此在送出 AJAX 之前需要先轉成真正的陣列。
  5. 轉成真正的陣列:透過 Array.from() 把剛剛的 Uint8Array 轉成真正的陣列。
  6. 轉成 JSON 格式:如果直接對 Uint8Array 執行 JSON.stringify() 會得到錯誤的結果,記得要先使用 Array.from() 才可以使用 JSON.stringify()
❗️透過 new Uint8Array() 轉換出來的陣列會是一個「類陣列(Typed Array)」,可以透過 Array.from() 等方式轉換成真正的陣列。

範例程式碼

JavaScript

See the Pen File Upload with JavaScript by PJCHEN (@PJCHENder) on CodePen.

JSX

// FileUploader.js
import React from 'react';

async function handleUpload(e) {

  // STEP 2: 得到該檔案的 Blob, i.e., e.target.files
  const arrayBuffer = await getArrayBuffer(e.target.files[0]);
  console.log('arrayBuffer', arrayBuffer);

  const response = await uploadFile(arrayBuffer);
  console.log('response', response);
}

function getArrayBuffer(file) {
  return new Promise((resolve, reject) => {
    // STEP 3: 轉成 ArrayBuffer, i.e., reader.result
    const reader = new FileReader();
    reader.addEventListener('load', () => {
      resolve(reader.result);
    });
    reader.readAsArrayBuffer(file);
  })
}

function uploadFile(arrayBuffer) {
  return fetch(`https://api.foobar.io`, {
    method: 'POST',

    // STEP 6:使用 JSON.stringify() 包起來送出
    body: JSON.stringify({
      appId: 3,
      format: 'png',

      // STEP 4:轉成 Uint8Array(這是 TypedArray)
      // STEP 5:透過 Array.from 轉成真正的陣列
      icon: Array.from(new Uint8Array(arrayBuffer)),
    }),
  }).then((res)=> {
    if (!res.ok) {
      throw res.statusText;
    }
    return res.json()
  })
  .then(({ data }) => console.log('data', data))
  .catch(err => console.log('err', err))
}

const FileUploader = () => {

  // STEP 1: 建立上傳表單
  return (
    <input type="file" onChange={handleUpload}/>
  )
}

export default FileUploader;

顯示預覽圖

取得欲覽圖的方式可以透過 fileReadercreateObjectURL

方法一: 使用 fileReader

onload 中的 callback,可以透過 e.target.result 取得該檔案。
const curFile = curFiles[0]; // 透過 input 取得的 file object
const reader = new FileReader();
reader.onload = function (e) {
  console.log('file:', e.target.result);
};

// 使用 readAsDataURL 將圖片轉成 Base64
reader.readAsDataURL(curFile);

方法二:使用 createObjectURL

const curFile = curFiles[0]; // 透過 input 取得的 file object
const objectURL = URL.createObjectURL(curFile);
console.log('objectURL', objectURL);

常用函式

returnFileSize

function returnFileSize(number) {
  if (number < 1024) {
    return `${number}bytes`;
  } if (number > 1024 && number < 1048576) {
    return `${(number / 1024).toFixed(1)}KB`;
  } if (number > 1048576) {
    return `${(number / 1048576).toFixed(1)}MB`;
  }
}

validFileType

function validFileType(file) {
  const acceptFileTypes = ["image/jpeg", "image/png"];
  const isValidFileType = acceptFileTypes.includes(fileObject.type);
  return isValidFileType;
}

表單清空

e.target.value = '';

參考資料

說明如何透過如何使用 input file, drag and drop, preview

Drag and Drop

API

相關閱讀

圖片來源:

  • jQuery File Upload Scripts