send video messages

This commit is contained in:
Bruno Windels 2021-03-09 19:35:25 +01:00
parent ee6f3e5457
commit c6ff56a942
4 changed files with 114 additions and 11 deletions

View file

@ -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() {
try {
if (!this.platform.hasReadPixelPermission()) {
@ -221,7 +258,9 @@ export class RoomViewModel extends ViewModel {
}
await this._room.sendEvent("m.room.message", content, attachments);
} 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();
}
sendVideo() {
this._roomVM._pickAndSendVideo();
}
get canSend() {
return !this._isEmpty;
}
@ -283,3 +326,9 @@ function imageToInfo(image) {
size: image.blob.size
};
}
function videoToInfo(video) {
const info = imageToInfo(video);
info.duration = video.duration;
return info;
}

View file

@ -32,7 +32,7 @@ import {Crypto} from "./dom/Crypto.js";
import {estimateStorageUsage} from "./dom/StorageEstimate.js";
import {WorkerPool} from "./dom/WorkerPool.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";
function addScript(src) {
@ -184,6 +184,10 @@ export class Platform {
return ImageHandle.fromBlob(blob);
}
async loadVideo(blob) {
return VideoHandle.fromBlob(blob);
}
hasReadPixelPermission() {
return hasReadPixelPermission();
}

View file

@ -27,18 +27,18 @@ export class ImageHandle {
this.blob = blob;
this.width = width;
this.height = height;
this._imgElement = imgElement;
this._domElement = imgElement;
}
get maxDimension() {
return Math.max(this.width, this.height);
}
async _getImgElement() {
if (!this._imgElement) {
this._imgElement = await loadImgFromBlob(this.blob);
async _getDomElement() {
if (!this._domElement) {
this._domElement = await loadImgFromBlob(this.blob);
}
return this._imgElement;
return this._domElement;
}
async scale(maxDimension) {
@ -46,18 +46,18 @@ export class ImageHandle {
const scaleFactor = Math.min(1, maxDimension / (aspectRatio >= 1 ? this.width : this.height));
const scaledWidth = Math.round(this.width * scaleFactor);
const scaledHeight = Math.round(this.height * scaleFactor);
const canvas = document.createElement("canvas");
canvas.width = scaledWidth;
canvas.height = scaledHeight;
const ctx = canvas.getContext("2d");
const img = await this._getImgElement();
ctx.drawImage(img, 0, 0, scaledWidth, scaledHeight);
const drawableElement = await this._getDomElement();
ctx.drawImage(drawableElement, 0, 0, scaledWidth, scaledHeight);
let mimeType = this.blob.mimeType === "image/jpeg" ? "image/jpeg" : "image/png";
let nativeBlob;
if (canvas.toBlob) {
nativeBlob = await new Promise(resolve => canvas.toBlob(resolve, mimeType));
} else if (canvas.msToBlob) {
// TODO: provide a mimetype override in blob handle for this case
mimeType = "image/png";
nativeBlob = canvas.msToBlob();
} 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() {
const canvas = document.createElement("canvas");
canvas.width = 1;
@ -91,7 +106,8 @@ export function hasReadPixelPermission() {
async function loadImgFromBlob(blob) {
const img = document.createElement("img");
let detach;
const loadPromise = new Promise((resolve, reject) => {
const loadPromise = new Promise((resolve, _reject) => {
const reject = evt => _reject(evt.target.error);
detach = () => {
img.removeEventListener("load", resolve);
img.removeEventListener("error", reject);
@ -104,3 +120,36 @@ async function loadImgFromBlob(blob) {
detach();
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;
}

View file

@ -66,6 +66,7 @@ export class MessageComposer extends TemplateView {
} else {
const vm = this.value;
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 file`, () => vm.sendFile()).setIcon("file"),
]));