FEATURE: Auto generate and display video preview image (#25633)

This change will allow auto generated video thumbnails to be used
instead of the black video thumbnail that overlays videos.

Follow up to: 2443446e62
This commit is contained in:
Blake Erickson 2024-02-14 13:43:53 -07:00
parent aac6036868
commit 0821b2b6fe
8 changed files with 147 additions and 63 deletions

View File

@ -187,7 +187,11 @@ function renderImageOrPlayableMedia(tokens, idx, options, env, slf) {
options.discourse.previewing &&
!options.discourse.limitedSiteSettings.enableDiffhtmlPreview
) {
return `<div class="onebox-placeholder-container">
const origSrc = token.attrGet("data-orig-src");
const origSrcId = origSrc
.substring(origSrc.lastIndexOf("/") + 1)
.split(".")[0];
return `<div class="onebox-placeholder-container" data-orig-src-id="${origSrcId}">
<span class="placeholder-icon video"></span>
</div>`;
} else {

View File

@ -80,6 +80,15 @@ export default {
);
containers.forEach((container) => {
// Add video thumbnail image
if (container.dataset.thumbnailSrc) {
const thumbnail = new Image();
thumbnail.onload = function () {
container.style.backgroundImage = "url('" + thumbnail.src + "')";
};
thumbnail.src = container.dataset.thumbnailSrc;
}
const wrapper = document.createElement("div"),
overlay = document.createElement("div");

View File

@ -31,9 +31,11 @@ export default class ComposerVideoThumbnailUppy extends EmberObject.extend(
uploadRootPath = "/uploads";
uploadTargetBound = false;
useUploadPlaceholders = true;
capabilities = null;
constructor(owner) {
super(...arguments);
this.capabilities = owner.lookup("service:capabilities");
setOwner(this, owner);
}
@ -55,13 +57,17 @@ export default class ComposerVideoThumbnailUppy extends EmberObject.extend(
video.muted = true;
video.playsinline = true;
let videoSha1 = uploadUrl
const videoSha1 = uploadUrl
.substring(uploadUrl.lastIndexOf("/") + 1)
.split(".")[0];
// Wait for the video element to load, otherwise the canvas will be empty.
// iOS Safari prefers onloadedmetadata over oncanplay.
video.onloadedmetadata = () => {
// iOS Safari prefers onloadedmetadata over oncanplay. System tests running in Chrome
// prefer oncanplaythrough.
const eventName = this.capabilities.isIOS
? "onloadedmetadata"
: "oncanplaythrough";
video[eventName] = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = video.videoWidth;
@ -70,7 +76,20 @@ export default class ComposerVideoThumbnailUppy extends EmberObject.extend(
// A timeout is needed on mobile.
setTimeout(() => {
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
// Detect Empty Thumbnail
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
let isEmpty = true;
for (let i = 0; i < data.length; i += 4) {
// Check RGB values
if (data[i] !== 0 || data[i + 1] !== 0 || data[i + 2] !== 0) {
isEmpty = false;
break;
}
}
if (!isEmpty) {
// upload video thumbnail
canvas.toBlob((blob) => {
this._uppyInstance = new Uppy({
@ -126,6 +145,10 @@ export default class ComposerVideoThumbnailUppy extends EmberObject.extend(
});
}
});
} else {
this.uploading = false;
callback();
}
}, 100);
};
}

View File

@ -1498,8 +1498,8 @@ var bar = 'bar';
assert.cookedOptions(
`![baby shark|video](upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp4)`,
{ previewing: true },
`<p><div class=\"onebox-placeholder-container\">
<span class=\"placeholder-icon video\"></span>
`<p><div class="onebox-placeholder-container" data-orig-src-id="eyPnj7UzkU0AkGkx2dx8G4YM1Jx">
<span class="placeholder-icon video"></span>
</div></p>`
);
});

View File

@ -935,6 +935,8 @@ aside.onebox.mixcloud-preview {
}
}
.video-placeholder-container {
background-size: cover;
background-position: center;
position: relative;
padding: 0 0 56.25% 0;
width: 100%;
@ -1000,6 +1002,8 @@ iframe.vimeo-onebox {
}
.onebox-placeholder-container {
background-size: cover;
background-position: center;
position: relative;
width: 100%;
padding: 0 0 48.25% 0;

View File

@ -1048,8 +1048,9 @@ class Post < ActiveRecord::Base
.where("original_filename like ?", "#{upload.sha1}.%")
.order(id: :desc)
.first if upload.sha1.present?
if thumbnail.present? && self.is_first_post? && !self.topic.image_upload_id
if thumbnail.present?
upload_ids << thumbnail.id
if self.is_first_post? && !self.topic.image_upload_id
self.topic.update_column(:image_upload_id, thumbnail.id)
extra_sizes =
ThemeModifierHelper.new(
@ -1058,6 +1059,7 @@ class Post < ActiveRecord::Base
self.topic.generate_thumbnails!(extra_sizes: extra_sizes)
end
end
end
upload_ids << upload.id if upload.present?
end

View File

@ -302,6 +302,7 @@ module PrettyText
add_rel_attributes_to_user_content(doc, add_nofollow)
strip_hidden_unicode_bidirectional_characters(doc)
sanitize_hotlinked_media(doc)
add_video_placeholder_image(doc)
add_mentions(doc, user_id: opts[:user_id]) if SiteSetting.enable_mentions
@ -442,6 +443,21 @@ module PrettyText
links
end
def self.add_video_placeholder_image(doc)
doc
.css(".video-placeholder-container")
.each do |video|
video_src = video["data-video-src"]
video_sha1 = File.basename(video_src, File.extname(video_src))
thumbnail = Upload.where("original_filename LIKE ?", "#{video_sha1}.%").last
if thumbnail
video["data-thumbnail-src"] = UrlHelper.absolute(
GlobalPath.upload_cdn_path(thumbnail.url),
)
end
end
end
def self.extract_mentions(cooked)
mentions =
cooked

View File

@ -51,7 +51,7 @@ describe "Uploading files in the composer", type: :system do
# we need to come back to this in a few months and try again.
#
# c.f. https://groups.google.com/g/chromedriver-users/c/1SMbByMfO2U
xit "generates a thumbnail for the video" do
xit "generates a topic preview thumbnail from the video" do
sign_in(current_user)
visit "/new-topic"
@ -62,12 +62,38 @@ describe "Uploading files in the composer", type: :system do
attach_file(file_path_1) { composer.click_toolbar_button("upload") }
expect(composer).to have_no_in_progress_uploads
expect(composer.preview).to have_css(".video-container")
expect(composer.preview).to have_css(".onebox-placeholder-container")
composer.submit
expect(find("#topic-title")).to have_content("Video upload test")
expect(topic.image_upload_id).to eq(Upload.last.id)
# I think topic list previews need to be enabled for this?
#expect(topic.image_upload_id).to eq(Upload.last.id)
end
it "generates a thumbnail from the video" do
sign_in(current_user)
visit "/new-topic"
expect(composer).to be_opened
topic.fill_in_composer_title("Video upload test")
file_path_1 = file_from_fixtures("small.mp4", "media").path
attach_file(file_path_1) { composer.click_toolbar_button("upload") }
expect(composer).to have_no_in_progress_uploads
expect(composer.preview).to have_css(".onebox-placeholder-container")
composer.submit
expect(find("#topic-title")).to have_content("Video upload test")
selector = topic.post_by_number_selector(1)
expect(page).to have_css(selector)
within(selector) do
expect(page).to have_css(".video-placeholder-container[data-thumbnail-src]")
end
end
end
end