send video messages
This commit is contained in:
parent
ee6f3e5457
commit
c6ff56a942
4 changed files with 114 additions and 11 deletions
|
@ -187,6 +187,43 @@ export class RoomViewModel extends ViewModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _pickAndSendVideo() {
|
||||||
|
try {
|
||||||
|
if (!this.platform.hasReadPixelPermission()) {
|
||||||
|
alert("Please allow canvas image data access, so we can scale your images down.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = await this.platform.openFile("video/*");
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!file.blob.mimeType.startsWith("video/")) {
|
||||||
|
return this._sendFile(file);
|
||||||
|
}
|
||||||
|
let video = await this.platform.loadVideo(file.blob);
|
||||||
|
const content = {
|
||||||
|
body: file.name,
|
||||||
|
msgtype: "m.video",
|
||||||
|
info: videoToInfo(video)
|
||||||
|
};
|
||||||
|
const attachments = {
|
||||||
|
"url": this._room.createAttachment(video.blob, file.name),
|
||||||
|
};
|
||||||
|
|
||||||
|
const limit = await this.platform.settingsStorage.getInt("sentImageSizeLimit");
|
||||||
|
const maxDimension = limit || Math.min(video.maxDimension, 800);
|
||||||
|
const thumbnail = await video.scale(maxDimension);
|
||||||
|
content.info.thumbnail_info = imageToInfo(thumbnail);
|
||||||
|
attachments["info.thumbnail_url"] =
|
||||||
|
this._room.createAttachment(thumbnail.blob, file.name);
|
||||||
|
await this._room.sendEvent("m.room.message", content, attachments);
|
||||||
|
} catch (err) {
|
||||||
|
this._sendError = err;
|
||||||
|
this.emitChange("error");
|
||||||
|
console.error(err.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _pickAndSendPicture() {
|
async _pickAndSendPicture() {
|
||||||
try {
|
try {
|
||||||
if (!this.platform.hasReadPixelPermission()) {
|
if (!this.platform.hasReadPixelPermission()) {
|
||||||
|
@ -221,7 +258,9 @@ export class RoomViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
await this._room.sendEvent("m.room.message", content, attachments);
|
await this._room.sendEvent("m.room.message", content, attachments);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
this._sendError = err;
|
||||||
|
this.emitChange("error");
|
||||||
|
console.error(err.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,6 +298,10 @@ class ComposerViewModel extends ViewModel {
|
||||||
this._roomVM._pickAndSendFile();
|
this._roomVM._pickAndSendFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendVideo() {
|
||||||
|
this._roomVM._pickAndSendVideo();
|
||||||
|
}
|
||||||
|
|
||||||
get canSend() {
|
get canSend() {
|
||||||
return !this._isEmpty;
|
return !this._isEmpty;
|
||||||
}
|
}
|
||||||
|
@ -283,3 +326,9 @@ function imageToInfo(image) {
|
||||||
size: image.blob.size
|
size: image.blob.size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function videoToInfo(video) {
|
||||||
|
const info = imageToInfo(video);
|
||||||
|
info.duration = video.duration;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ import {Crypto} from "./dom/Crypto.js";
|
||||||
import {estimateStorageUsage} from "./dom/StorageEstimate.js";
|
import {estimateStorageUsage} from "./dom/StorageEstimate.js";
|
||||||
import {WorkerPool} from "./dom/WorkerPool.js";
|
import {WorkerPool} from "./dom/WorkerPool.js";
|
||||||
import {BlobHandle} from "./dom/BlobHandle.js";
|
import {BlobHandle} from "./dom/BlobHandle.js";
|
||||||
import {hasReadPixelPermission, ImageHandle} from "./dom/ImageHandle.js";
|
import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js";
|
||||||
import {downloadInIframe} from "./dom/download.js";
|
import {downloadInIframe} from "./dom/download.js";
|
||||||
|
|
||||||
function addScript(src) {
|
function addScript(src) {
|
||||||
|
@ -184,6 +184,10 @@ export class Platform {
|
||||||
return ImageHandle.fromBlob(blob);
|
return ImageHandle.fromBlob(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadVideo(blob) {
|
||||||
|
return VideoHandle.fromBlob(blob);
|
||||||
|
}
|
||||||
|
|
||||||
hasReadPixelPermission() {
|
hasReadPixelPermission() {
|
||||||
return hasReadPixelPermission();
|
return hasReadPixelPermission();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,18 +27,18 @@ export class ImageHandle {
|
||||||
this.blob = blob;
|
this.blob = blob;
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this._imgElement = imgElement;
|
this._domElement = imgElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxDimension() {
|
get maxDimension() {
|
||||||
return Math.max(this.width, this.height);
|
return Math.max(this.width, this.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getImgElement() {
|
async _getDomElement() {
|
||||||
if (!this._imgElement) {
|
if (!this._domElement) {
|
||||||
this._imgElement = await loadImgFromBlob(this.blob);
|
this._domElement = await loadImgFromBlob(this.blob);
|
||||||
}
|
}
|
||||||
return this._imgElement;
|
return this._domElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
async scale(maxDimension) {
|
async scale(maxDimension) {
|
||||||
|
@ -46,18 +46,18 @@ export class ImageHandle {
|
||||||
const scaleFactor = Math.min(1, maxDimension / (aspectRatio >= 1 ? this.width : this.height));
|
const scaleFactor = Math.min(1, maxDimension / (aspectRatio >= 1 ? this.width : this.height));
|
||||||
const scaledWidth = Math.round(this.width * scaleFactor);
|
const scaledWidth = Math.round(this.width * scaleFactor);
|
||||||
const scaledHeight = Math.round(this.height * scaleFactor);
|
const scaledHeight = Math.round(this.height * scaleFactor);
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = scaledWidth;
|
canvas.width = scaledWidth;
|
||||||
canvas.height = scaledHeight;
|
canvas.height = scaledHeight;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
const img = await this._getImgElement();
|
const drawableElement = await this._getDomElement();
|
||||||
ctx.drawImage(img, 0, 0, scaledWidth, scaledHeight);
|
ctx.drawImage(drawableElement, 0, 0, scaledWidth, scaledHeight);
|
||||||
let mimeType = this.blob.mimeType === "image/jpeg" ? "image/jpeg" : "image/png";
|
let mimeType = this.blob.mimeType === "image/jpeg" ? "image/jpeg" : "image/png";
|
||||||
let nativeBlob;
|
let nativeBlob;
|
||||||
if (canvas.toBlob) {
|
if (canvas.toBlob) {
|
||||||
nativeBlob = await new Promise(resolve => canvas.toBlob(resolve, mimeType));
|
nativeBlob = await new Promise(resolve => canvas.toBlob(resolve, mimeType));
|
||||||
} else if (canvas.msToBlob) {
|
} else if (canvas.msToBlob) {
|
||||||
|
// TODO: provide a mimetype override in blob handle for this case
|
||||||
mimeType = "image/png";
|
mimeType = "image/png";
|
||||||
nativeBlob = canvas.msToBlob();
|
nativeBlob = canvas.msToBlob();
|
||||||
} else {
|
} else {
|
||||||
|
@ -72,6 +72,21 @@ export class ImageHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class VideoHandle extends ImageHandle {
|
||||||
|
get duration() {
|
||||||
|
if (typeof this._domElement.duration === "number") {
|
||||||
|
return Math.round(this._domElement.duration * 1000);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fromBlob(blob) {
|
||||||
|
const video = await loadVideoFromBlob(blob);
|
||||||
|
const {videoWidth, videoHeight} = video;
|
||||||
|
return new VideoHandle(blob, videoWidth, videoHeight, video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function hasReadPixelPermission() {
|
export function hasReadPixelPermission() {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = 1;
|
canvas.width = 1;
|
||||||
|
@ -91,7 +106,8 @@ export function hasReadPixelPermission() {
|
||||||
async function loadImgFromBlob(blob) {
|
async function loadImgFromBlob(blob) {
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
let detach;
|
let detach;
|
||||||
const loadPromise = new Promise((resolve, reject) => {
|
const loadPromise = new Promise((resolve, _reject) => {
|
||||||
|
const reject = evt => _reject(evt.target.error);
|
||||||
detach = () => {
|
detach = () => {
|
||||||
img.removeEventListener("load", resolve);
|
img.removeEventListener("load", resolve);
|
||||||
img.removeEventListener("error", reject);
|
img.removeEventListener("error", reject);
|
||||||
|
@ -104,3 +120,36 @@ async function loadImgFromBlob(blob) {
|
||||||
detach();
|
detach();
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadVideoFromBlob(blob) {
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.muted = true;
|
||||||
|
let detach;
|
||||||
|
const loadPromise = new Promise((resolve, _reject) => {
|
||||||
|
const reject = evt => _reject(evt.target.error);
|
||||||
|
detach = () => {
|
||||||
|
video.removeEventListener("loadedmetadata", resolve);
|
||||||
|
video.removeEventListener("error", reject);
|
||||||
|
};
|
||||||
|
video.addEventListener("loadedmetadata", resolve);
|
||||||
|
video.addEventListener("error", reject);
|
||||||
|
});
|
||||||
|
video.src = blob.url;
|
||||||
|
video.load();
|
||||||
|
await loadPromise;
|
||||||
|
// seek to the first 1/10s to make sure that drawing the video
|
||||||
|
// on a canvas won't give a blank image
|
||||||
|
const seekPromise = new Promise((resolve, _reject) => {
|
||||||
|
const reject = evt => _reject(evt.target.error);
|
||||||
|
detach = () => {
|
||||||
|
video.removeEventListener("seeked", resolve);
|
||||||
|
video.removeEventListener("error", reject);
|
||||||
|
};
|
||||||
|
video.addEventListener("seeked", resolve);
|
||||||
|
video.addEventListener("error", reject);
|
||||||
|
});
|
||||||
|
video.currentTime = 0.1;
|
||||||
|
await seekPromise;
|
||||||
|
detach();
|
||||||
|
return video;
|
||||||
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ export class MessageComposer extends TemplateView {
|
||||||
} else {
|
} else {
|
||||||
const vm = this.value;
|
const vm = this.value;
|
||||||
this._attachmentPopup = new Popup(new Menu([
|
this._attachmentPopup = new Popup(new Menu([
|
||||||
|
Menu.option(vm.i18n`Send video`, () => vm.sendVideo()).setIcon("video"),
|
||||||
Menu.option(vm.i18n`Send picture`, () => vm.sendPicture()).setIcon("picture"),
|
Menu.option(vm.i18n`Send picture`, () => vm.sendPicture()).setIcon("picture"),
|
||||||
Menu.option(vm.i18n`Send file`, () => vm.sendFile()).setIcon("file"),
|
Menu.option(vm.i18n`Send file`, () => vm.sendFile()).setIcon("file"),
|
||||||
]));
|
]));
|
||||||
|
|
Reference in a new issue