mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 03:23:38 +08:00
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:
parent
aac6036868
commit
0821b2b6fe
|
@ -187,7 +187,11 @@ function renderImageOrPlayableMedia(tokens, idx, options, env, slf) {
|
||||||
options.discourse.previewing &&
|
options.discourse.previewing &&
|
||||||
!options.discourse.limitedSiteSettings.enableDiffhtmlPreview
|
!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>
|
<span class="placeholder-icon video"></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -80,6 +80,15 @@ export default {
|
||||||
);
|
);
|
||||||
|
|
||||||
containers.forEach((container) => {
|
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"),
|
const wrapper = document.createElement("div"),
|
||||||
overlay = document.createElement("div");
|
overlay = document.createElement("div");
|
||||||
|
|
||||||
|
|
|
@ -31,9 +31,11 @@ export default class ComposerVideoThumbnailUppy extends EmberObject.extend(
|
||||||
uploadRootPath = "/uploads";
|
uploadRootPath = "/uploads";
|
||||||
uploadTargetBound = false;
|
uploadTargetBound = false;
|
||||||
useUploadPlaceholders = true;
|
useUploadPlaceholders = true;
|
||||||
|
capabilities = null;
|
||||||
|
|
||||||
constructor(owner) {
|
constructor(owner) {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
this.capabilities = owner.lookup("service:capabilities");
|
||||||
setOwner(this, owner);
|
setOwner(this, owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,13 +57,17 @@ export default class ComposerVideoThumbnailUppy extends EmberObject.extend(
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.playsinline = true;
|
video.playsinline = true;
|
||||||
|
|
||||||
let videoSha1 = uploadUrl
|
const videoSha1 = uploadUrl
|
||||||
.substring(uploadUrl.lastIndexOf("/") + 1)
|
.substring(uploadUrl.lastIndexOf("/") + 1)
|
||||||
.split(".")[0];
|
.split(".")[0];
|
||||||
|
|
||||||
// Wait for the video element to load, otherwise the canvas will be empty.
|
// Wait for the video element to load, otherwise the canvas will be empty.
|
||||||
// iOS Safari prefers onloadedmetadata over oncanplay.
|
// iOS Safari prefers onloadedmetadata over oncanplay. System tests running in Chrome
|
||||||
video.onloadedmetadata = () => {
|
// prefer oncanplaythrough.
|
||||||
|
const eventName = this.capabilities.isIOS
|
||||||
|
? "onloadedmetadata"
|
||||||
|
: "oncanplaythrough";
|
||||||
|
video[eventName] = () => {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
canvas.width = video.videoWidth;
|
canvas.width = video.videoWidth;
|
||||||
|
@ -70,62 +76,79 @@ export default class ComposerVideoThumbnailUppy extends EmberObject.extend(
|
||||||
// A timeout is needed on mobile.
|
// A timeout is needed on mobile.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
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;
|
||||||
|
|
||||||
// upload video thumbnail
|
let isEmpty = true;
|
||||||
canvas.toBlob((blob) => {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
this._uppyInstance = new Uppy({
|
// Check RGB values
|
||||||
id: "video-thumbnail",
|
if (data[i] !== 0 || data[i + 1] !== 0 || data[i + 2] !== 0) {
|
||||||
meta: {
|
isEmpty = false;
|
||||||
videoSha1,
|
break;
|
||||||
upload_type: "thumbnail",
|
|
||||||
},
|
|
||||||
autoProceed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.siteSettings.enable_upload_debug_mode) {
|
|
||||||
this._instrumentUploadTimings();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.siteSettings.enable_direct_s3_uploads) {
|
if (!isEmpty) {
|
||||||
this._useS3MultipartUploads();
|
// upload video thumbnail
|
||||||
} else {
|
canvas.toBlob((blob) => {
|
||||||
this._useXHRUploads();
|
this._uppyInstance = new Uppy({
|
||||||
}
|
id: "video-thumbnail",
|
||||||
|
meta: {
|
||||||
|
videoSha1,
|
||||||
|
upload_type: "thumbnail",
|
||||||
|
},
|
||||||
|
autoProceed: true,
|
||||||
|
});
|
||||||
|
|
||||||
this._uppyInstance.on("upload", () => {
|
if (this.siteSettings.enable_upload_debug_mode) {
|
||||||
this.uploading = true;
|
this._instrumentUploadTimings();
|
||||||
});
|
|
||||||
|
|
||||||
this._uppyInstance.on("upload-success", () => {
|
|
||||||
this.uploading = false;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
|
|
||||||
this._uppyInstance.on("upload-error", (file, error, response) => {
|
|
||||||
let message = I18n.t("wizard.upload_error");
|
|
||||||
if (response.body.errors) {
|
|
||||||
message = response.body.errors.join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
if (this.siteSettings.enable_direct_s3_uploads) {
|
||||||
console.error(message);
|
this._useS3MultipartUploads();
|
||||||
this.uploading = false;
|
} else {
|
||||||
callback();
|
this._useXHRUploads();
|
||||||
});
|
}
|
||||||
|
|
||||||
try {
|
this._uppyInstance.on("upload", () => {
|
||||||
this._uppyInstance.addFile({
|
this.uploading = true;
|
||||||
source: `${this.id}-video-thumbnail`,
|
|
||||||
name: `${videoSha1}`,
|
|
||||||
type: blob.type,
|
|
||||||
data: blob,
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
warn(`error adding files to uppy: ${err}`, {
|
this._uppyInstance.on("upload-success", () => {
|
||||||
id: "discourse.upload.uppy-add-files-error",
|
this.uploading = false;
|
||||||
|
callback();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
this._uppyInstance.on("upload-error", (file, error, response) => {
|
||||||
|
let message = I18n.t("wizard.upload_error");
|
||||||
|
if (response.body.errors) {
|
||||||
|
message = response.body.errors.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(message);
|
||||||
|
this.uploading = false;
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._uppyInstance.addFile({
|
||||||
|
source: `${this.id}-video-thumbnail`,
|
||||||
|
name: `${videoSha1}`,
|
||||||
|
type: blob.type,
|
||||||
|
data: blob,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
warn(`error adding files to uppy: ${err}`, {
|
||||||
|
id: "discourse.upload.uppy-add-files-error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.uploading = false;
|
||||||
|
callback();
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1498,8 +1498,8 @@ var bar = 'bar';
|
||||||
assert.cookedOptions(
|
assert.cookedOptions(
|
||||||
`![baby shark|video](upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp4)`,
|
`![baby shark|video](upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp4)`,
|
||||||
{ previewing: true },
|
{ previewing: true },
|
||||||
`<p><div class=\"onebox-placeholder-container\">
|
`<p><div class="onebox-placeholder-container" data-orig-src-id="eyPnj7UzkU0AkGkx2dx8G4YM1Jx">
|
||||||
<span class=\"placeholder-icon video\"></span>
|
<span class="placeholder-icon video"></span>
|
||||||
</div></p>`
|
</div></p>`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -935,6 +935,8 @@ aside.onebox.mixcloud-preview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.video-placeholder-container {
|
.video-placeholder-container {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 0 56.25% 0;
|
padding: 0 0 56.25% 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -1000,6 +1002,8 @@ iframe.vimeo-onebox {
|
||||||
}
|
}
|
||||||
|
|
||||||
.onebox-placeholder-container {
|
.onebox-placeholder-container {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 0 48.25% 0;
|
padding: 0 0 48.25% 0;
|
||||||
|
|
|
@ -1048,14 +1048,16 @@ class Post < ActiveRecord::Base
|
||||||
.where("original_filename like ?", "#{upload.sha1}.%")
|
.where("original_filename like ?", "#{upload.sha1}.%")
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
.first if upload.sha1.present?
|
.first if upload.sha1.present?
|
||||||
if thumbnail.present? && self.is_first_post? && !self.topic.image_upload_id
|
if thumbnail.present?
|
||||||
upload_ids << thumbnail.id
|
upload_ids << thumbnail.id
|
||||||
self.topic.update_column(:image_upload_id, thumbnail.id)
|
if self.is_first_post? && !self.topic.image_upload_id
|
||||||
extra_sizes =
|
self.topic.update_column(:image_upload_id, thumbnail.id)
|
||||||
ThemeModifierHelper.new(
|
extra_sizes =
|
||||||
theme_ids: Theme.user_selectable.pluck(:id),
|
ThemeModifierHelper.new(
|
||||||
).topic_thumbnail_sizes
|
theme_ids: Theme.user_selectable.pluck(:id),
|
||||||
self.topic.generate_thumbnails!(extra_sizes: extra_sizes)
|
).topic_thumbnail_sizes
|
||||||
|
self.topic.generate_thumbnails!(extra_sizes: extra_sizes)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
upload_ids << upload.id if upload.present?
|
upload_ids << upload.id if upload.present?
|
||||||
|
|
|
@ -302,6 +302,7 @@ module PrettyText
|
||||||
add_rel_attributes_to_user_content(doc, add_nofollow)
|
add_rel_attributes_to_user_content(doc, add_nofollow)
|
||||||
strip_hidden_unicode_bidirectional_characters(doc)
|
strip_hidden_unicode_bidirectional_characters(doc)
|
||||||
sanitize_hotlinked_media(doc)
|
sanitize_hotlinked_media(doc)
|
||||||
|
add_video_placeholder_image(doc)
|
||||||
|
|
||||||
add_mentions(doc, user_id: opts[:user_id]) if SiteSetting.enable_mentions
|
add_mentions(doc, user_id: opts[:user_id]) if SiteSetting.enable_mentions
|
||||||
|
|
||||||
|
@ -442,6 +443,21 @@ module PrettyText
|
||||||
links
|
links
|
||||||
end
|
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)
|
def self.extract_mentions(cooked)
|
||||||
mentions =
|
mentions =
|
||||||
cooked
|
cooked
|
||||||
|
|
|
@ -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.
|
# 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
|
# 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)
|
sign_in(current_user)
|
||||||
|
|
||||||
visit "/new-topic"
|
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") }
|
attach_file(file_path_1) { composer.click_toolbar_button("upload") }
|
||||||
|
|
||||||
expect(composer).to have_no_in_progress_uploads
|
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
|
composer.submit
|
||||||
|
|
||||||
expect(find("#topic-title")).to have_content("Video upload test")
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user