diff --git a/src/main/java/org/olat/core/commons/services/video/JCodecHelper.java b/src/main/java/org/olat/core/commons/services/video/JCodecHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..46029699c5ca3988e598cc5ca149d07231896977 --- /dev/null +++ b/src/main/java/org/olat/core/commons/services/video/JCodecHelper.java @@ -0,0 +1,63 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.core.commons.services.video; + +import java.awt.image.BufferedImage; + +import org.apache.logging.log4j.Logger; +import org.jcodec.api.transcode.PixelStore; +import org.jcodec.api.transcode.PixelStore.LoanerPicture; +import org.jcodec.api.transcode.PixelStoreImpl; +import org.jcodec.api.transcode.filters.ScaleFilter; +import org.jcodec.common.model.Picture; +import org.jcodec.scale.AWTUtil; +import org.olat.core.commons.services.image.Size; +import org.olat.core.logging.Tracing; + +/** + * + * Initial date: 7 mars 2021<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class JCodecHelper { + + private static final Logger log = Tracing.createLoggerFor(JCodecHelper.class); + + private JCodecHelper() { + // + } + + public static BufferedImage scale(Size movieSize, Picture picture, BufferedImage bufImg) { + if(movieSize != null && picture != null && bufImg != null + && (bufImg.getWidth() != movieSize.getWidth() || bufImg.getHeight() != movieSize.getHeight())) { + try { + ScaleFilter filter = new ScaleFilter(movieSize.getWidth(), movieSize.getHeight()); + PixelStore store = new PixelStoreImpl(); + LoanerPicture lPicture = filter.filter(picture, store); + bufImg = AWTUtil.toBufferedImage(lPicture.getPicture()); + } catch (Exception e) { + log.error("", e); + } + } + return bufImg; + } + +} diff --git a/src/main/java/org/olat/core/commons/services/video/MovieServiceImpl.java b/src/main/java/org/olat/core/commons/services/video/MovieServiceImpl.java index 6d85c13da70a3f62b8064bb009b02a131b3821da..54f31594aa5b9d51d0a92fce394a2743791f7415 100644 --- a/src/main/java/org/olat/core/commons/services/video/MovieServiceImpl.java +++ b/src/main/java/org/olat/core/commons/services/video/MovieServiceImpl.java @@ -33,6 +33,7 @@ import org.jcodec.api.FrameGrab; import org.jcodec.common.Codec; import org.jcodec.common.VideoCodecMeta; import org.jcodec.common.io.FileChannelWrapper; +import org.jcodec.common.model.Picture; import org.jcodec.containers.mp4.boxes.MovieBox; import org.jcodec.containers.mp4.demuxer.MP4Demuxer; import org.jcodec.scale.AWTUtil; @@ -102,11 +103,12 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { FileChannelWrapper in = new FileChannelWrapper(ch); MP4Demuxer demuxer1 = MP4Demuxer.createMP4Demuxer(in)) { - - org.jcodec.common.model.Size size = demuxer1.getMovie().getDisplaySize(); + MovieBox movieBox = demuxer1.getMovie(); + org.jcodec.common.model.Size size = movieBox.getDisplaySize(); // Case 1: standard case, get dimension from movie int w = size.getWidth(); int h = size.getHeight(); + // Case 2: landscape movie from iOS: width and height is negative, no dunny why if (w < 0 && h < 0) { w = 0 - w; @@ -131,7 +133,7 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { } return new Size(w, h, false); } catch (Exception | AssertionError e) { - log.error("Cannot extract size of: " + media, e); + log.error("Cannot extract size of: {}", media, e); } } else if(suffix.equals("flv")) { try(InputStream stream = new FileInputStream(file)) { @@ -143,7 +145,7 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { return new Size(w, h, false); } } catch (Exception e) { - log.error("Cannot extract size of: " + media, e); + log.error("Cannot extract size of: {}", media, e); } } @@ -178,7 +180,7 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { // Simple calculation. Ignore NTSC and other issues for now return duration / timescale * 1000l; } catch (Exception | AssertionError e) { - log.error("Cannot extract duration of: " + media, e); + log.error("Cannot extract duration of: {}", media, e); } } @@ -205,7 +207,7 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { MP4Demuxer demuxer1 = MP4Demuxer.createMP4Demuxer(in)) { return demuxer1.getVideoTrack().getMeta().getTotalFrames(); } catch (Exception | AssertionError e) { - log.error("Cannot extract num. of frames of: " + media, e); + log.error("Cannot extract num. of frames of: {}", media, e); } } @@ -252,9 +254,12 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { WorkThreadInformations.setInfoFiles(null, file); WorkThreadInformations.set("Generate thumbnail (video) VFSLeaf=" + file); - File baseFile = ((LocalFileImpl)file).getBasefile(); + File movieFile = ((LocalFileImpl)file).getBasefile(); + Size movieSize = getSize(file, "mp4"); File scaledImage = ((LocalFileImpl)thumbnailFile).getBasefile(); - BufferedImage frame = AWTUtil.toBufferedImage(FrameGrab.getFrameFromFile(baseFile, 20)); + Picture picture = FrameGrab.getFrameFromFile(movieFile, 20); + BufferedImage frame = AWTUtil.toBufferedImage(picture); + frame = JCodecHelper.scale(movieSize, picture, frame); Size scaledSize = ImageHelperImpl.calcScaledSize(frame, maxWidth, maxHeight); if(ImageHelperImpl.writeTo(frame, scaledImage, scaledSize, "jpeg")) { size = new FinalSize(scaledSize.getWidth(), scaledSize.getHeight()); @@ -269,4 +274,6 @@ public class MovieServiceImpl implements MovieService, ThumbnailSPI { } return size; } + + } \ No newline at end of file diff --git a/src/main/java/org/olat/core/util/FileUtils.java b/src/main/java/org/olat/core/util/FileUtils.java index c2f8cffff3a051d7d6ad4b6854c9bed89920f061..4bb92042e8d17b75488fd7db611f0e11ec4cd969 100644 --- a/src/main/java/org/olat/core/util/FileUtils.java +++ b/src/main/java/org/olat/core/util/FileUtils.java @@ -805,7 +805,7 @@ public class FileUtils { } } //check if there are any unwanted path denominators in the name - if (filename.indexOf("..") > -1) { + if (filename.indexOf("..") > -1 || filename.startsWith(".")) { return false; } return true; @@ -856,6 +856,10 @@ public class FileUtils { } private static String cleanFilenamePart(String filename) { + while(filename.startsWith(".")) { + filename = filename.substring(1, filename.length()); + } + String cleaned = Normalizer.normalize(filename, Normalizer.Form.NFKD); cleaned = cleaned.replaceAll("\\p{InCombiningDiacriticalMarks}+",""); for (char character: FILE_NAME_FORBIDDEN_CHARS) { diff --git a/src/main/java/org/olat/course/nodes/iq/QTI21IdentityListCourseNodeToolsController.java b/src/main/java/org/olat/course/nodes/iq/QTI21IdentityListCourseNodeToolsController.java index ac72b5777eb659cc10a326f78958658564c0ff5d..7ce2075705b4e049d424d3aedd1e7df2acf42e56 100644 --- a/src/main/java/org/olat/course/nodes/iq/QTI21IdentityListCourseNodeToolsController.java +++ b/src/main/java/org/olat/course/nodes/iq/QTI21IdentityListCourseNodeToolsController.java @@ -314,7 +314,7 @@ public class QTI21IdentityListCourseNodeToolsController extends AbstractToolsCon Map<Identity, TestSessionState> testSessionStates = new HashMap<>(); testSessionStates.put(assessedIdentity, testSessionState); CorrectionOverviewModel model = new CorrectionOverviewModel(courseEntry, testCourseNode, testEntry, - resolvedAssessmentTest, manifestBuilder, lastSessionMap, testSessionStates); + resolvedAssessmentTest, manifestBuilder, lastSessionMap, testSessionStates, getTranslator()); correctionCtrl = new CorrectionIdentityAssessmentItemListController(ureq, getWindowControl(), stackPanel, model, assessedIdentity, assessmentEntryDone); listenTo(correctionCtrl); diff --git a/src/main/java/org/olat/course/nodes/pf/manager/FileSystemExport.java b/src/main/java/org/olat/course/nodes/pf/manager/FileSystemExport.java index 02c49ab77c6540f3a10d11c5988a913fa7d48df3..bd5962592efde6346ef0589558230c9e6454a615 100644 --- a/src/main/java/org/olat/course/nodes/pf/manager/FileSystemExport.java +++ b/src/main/java/org/olat/course/nodes/pf/manager/FileSystemExport.java @@ -29,10 +29,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -40,6 +43,7 @@ import java.util.zip.ZipOutputStream; import javax.servlet.http.HttpServletResponse; import org.apache.logging.log4j.Logger; +import org.olat.basesecurity.BaseSecurity; import org.olat.core.CoreSpringFactory; import org.olat.core.gui.media.MediaResource; import org.olat.core.gui.media.ServletUtil; @@ -55,7 +59,6 @@ import org.olat.course.nodes.PFCourseNode; import org.olat.course.nodes.pf.ui.PFParticipantController; import org.olat.course.run.environment.CourseEnvironment; import org.olat.repository.RepositoryEntry; -import org.olat.user.UserManager; /** * * Initial date: 15.12.2016<br> @@ -157,20 +160,35 @@ public class FileSystemExport implements MediaResource { } final String targetPath = zipPath; - final UserManager userManager = CoreSpringFactory.getImpl(UserManager.class); + Set<String> idKeys = new HashSet<>(); + Map<String, Identity> idMap = new HashMap<>(); if (identities != null) { for (Identity identity : identities) { - idKeys.add(identity.getKey().toString()); + String identityKey = identity.getKey().toString(); + idKeys.add(identityKey); + idMap.put(identityKey, identity); } } else { File[] listOfDirectories = sourceFolder.toFile().listFiles(SystemFileFilter.DIRECTORY_ONLY); if(listOfDirectories != null) { + List<Long> idKeysList = new ArrayList<>(); for (File file : listOfDirectories) { - idKeys.add(file.getName()); + String filename = file.getName(); + if(StringHelper.isLong(filename)) { + idKeys.add(filename); + idKeysList.add(Long.valueOf(filename)); + } + } + final BaseSecurity securityManager = CoreSpringFactory.getImpl(BaseSecurity.class); + List<Identity> loadedIdentities = securityManager.loadIdentityByKeys(idKeysList); + for (Identity identity : loadedIdentities) { + String identityKey = identity.getKey().toString(); + idMap.put(identityKey, identity); } } } + try { Files.walkFileTree(sourceFolder, new SimpleFileVisitor<Path>() { //contains identity check and changes identity key to user display name @@ -178,8 +196,14 @@ public class FileSystemExport implements MediaResource { for (String key : idKeys) { //additional check if folder is a identity-key (coming from fs) if (relPath.contains(key) && StringHelper.isLong(key)) { - String exportFolderName = userManager.getUserDisplayName(Long.parseLong(key)).replace(", ", "_") - + "_" + key; + String exportFolderName; + if(idMap.containsKey(key)) { + Identity id = idMap.get(key); + exportFolderName = (id.getUser().getLastName() + "_" + id.getUser().getFirstName()); + } else { + exportFolderName = ""; + } + exportFolderName = exportFolderName.replace(", ", "_") + "_" + key; return relPath.replace(key, exportFolderName); } } @@ -199,7 +223,9 @@ public class FileSystemExport implements MediaResource { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String relPath = sourceFolder.relativize(file).toString(); - if ((relPath = containsID(relPath)) != null && (relPath = boxesEnabled(relPath)) != null) { + if ((relPath = containsID(relPath)) != null + && (relPath = boxesEnabled(relPath)) != null + && !file.toFile().isHidden()) { zout.putNextEntry(new ZipEntry(targetPath + relPath)); copyFile(file, zout); zout.closeEntry(); diff --git a/src/main/java/org/olat/ims/qti21/ui/QTI21AssessmentDetailsController.java b/src/main/java/org/olat/ims/qti21/ui/QTI21AssessmentDetailsController.java index eb129c2b63c025e29db49d38ecf0f705e05cf7aa..0aed78139fe192eeaaecb0522e4e93be0bbbda8d 100644 --- a/src/main/java/org/olat/ims/qti21/ui/QTI21AssessmentDetailsController.java +++ b/src/main/java/org/olat/ims/qti21/ui/QTI21AssessmentDetailsController.java @@ -480,7 +480,7 @@ public class QTI21AssessmentDetailsController extends FormBasicController { testSessionStates.put(assessedIdentity, testSessionState); boolean correctionReadOnly = readOnly || assessmentEntryDone; CorrectionOverviewModel model = new CorrectionOverviewModel(entry, courseNode, testEntry, - resolvedAssessmentTest, manifestBuilder, lastSessions, testSessionStates); + resolvedAssessmentTest, manifestBuilder, lastSessions, testSessionStates, getTranslator()); correctionCtrl = new CorrectionIdentityAssessmentItemListController(ureq, getWindowControl(), stackPanel, model, assessedIdentity, correctionReadOnly); listenTo(correctionCtrl); diff --git a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionAssessmentItemListController.java b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionAssessmentItemListController.java index 12ca5fd64c525c49e1b6e5588d6fadac23906e52..e62983cf31b74a0630e15369f85a74f436601a15 100644 --- a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionAssessmentItemListController.java +++ b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionAssessmentItemListController.java @@ -411,7 +411,6 @@ public class CorrectionAssessmentItemListController extends FormBasicController } //reorder to match the list of identities - int count = 1; List<Identity> assessedIdentities = model.getAssessedIdentities(); List<AssessmentItemListEntry> reorderItemSessions = new ArrayList<>(assessedIdentities.size()); for(Identity assessedIdentity:assessedIdentities) { @@ -424,7 +423,7 @@ public class CorrectionAssessmentItemListController extends FormBasicController String title; if(anonymous) { - title = translate("number.assessed.identity", new String[] { Integer.toString(count++)} ); + title = model.getAnonymizedName(assessedIdentity); } else { title = userManager.getUserDisplayName(assessedIdentity); } diff --git a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionIdentityListController.java b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionIdentityListController.java index ab80e635b0d1f35ef1a7cde756f8bbf926b791c1..95149d69537c4b9bbd66cc00de01964e260cbb14 100644 --- a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionIdentityListController.java +++ b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionIdentityListController.java @@ -219,15 +219,14 @@ public class CorrectionIdentityListController extends FormBasicController { } } - int count = 0; List<CorrectionIdentityRow> rows = new ArrayList<>(model.getNumberOfAssessedIdentities()); Map<Identity, CorrectionIdentityRow> identityToRows = new HashMap<>(); for(Map.Entry<Identity, AssessmentTestSession> entry:model.getLastSessions().entrySet()) { - TestSessionState testSessionState = model.getTestSessionStates().get(entry.getKey()); + Identity assessedIdentity = entry.getKey(); + TestSessionState testSessionState = model.getTestSessionStates().get(assessedIdentity); if(testSessionState != null) { - String user = translate("number.assessed.identity", new String[]{ Integer.toString(++count) }); - CorrectionIdentityRow row = new CorrectionIdentityRow(user, entry.getKey(), entry.getValue(), userPropertyHandlers, getLocale()); - rows.add(row); + String user = model.getAnonymizedName(assessedIdentity); + CorrectionIdentityRow row = new CorrectionIdentityRow(user, assessedIdentity, entry.getValue(), userPropertyHandlers, getLocale()); identityToRows.put(entry.getKey(), row); for(Map.Entry<TestPlanNodeKey, ItemSessionState> itemEntry:testSessionState.getItemSessionStates().entrySet()) { @@ -239,6 +238,16 @@ public class CorrectionIdentityListController extends FormBasicController { } } + for(Identity assessedIdentity:model.getAssessedIdentities()) { + CorrectionIdentityRow row = identityToRows.remove(assessedIdentity); + if(row != null) { + rows.add(row); + } + } + if(!identityToRows.isEmpty()) { + rows.addAll(identityToRows.values()); + } + tableModel.setObjects(rows); tableEl.reset(reset, reset, true); } diff --git a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionOverviewController.java b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionOverviewController.java index 206a334d09253e645b7f5138ccf5d7891eec1b92..08295ae7a04134b7959b5ecc1edb4aae2e250e84 100644 --- a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionOverviewController.java +++ b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionOverviewController.java @@ -111,7 +111,7 @@ public class CorrectionOverviewController extends BasicController implements Too List<Identity> assessedIdentities = initializeAssessedIdentities(); model = new CorrectionOverviewModel(courseEntry, courseNode, testEntry, - resolvedAssessmentTest, manifestBuilder, assessedIdentities); + resolvedAssessmentTest, manifestBuilder, assessedIdentities, getTranslator()); segmentButtonsCmp = new ButtonGroupComponent("segments"); assessmentItemsLink = LinkFactory.createLink("correction.assessment.items", getTranslator(), this); diff --git a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionOverviewModel.java b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionOverviewModel.java index 17dedb9adb07dc61529e2f941f6b2905238b8479..1f183f489b52e4c86d889c79bfa28c62f5c9ad28 100644 --- a/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionOverviewModel.java +++ b/src/main/java/org/olat/ims/qti21/ui/assessment/CorrectionOverviewModel.java @@ -30,8 +30,10 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.Logger; import org.olat.core.CoreSpringFactory; +import org.olat.core.gui.translator.Translator; import org.olat.core.id.Identity; import org.olat.core.logging.Tracing; +import org.olat.core.util.StringHelper; import org.olat.course.CourseFactory; import org.olat.course.assessment.AssessmentHelper; import org.olat.course.assessment.CourseAssessmentService; @@ -47,6 +49,7 @@ import org.olat.modules.assessment.model.AssessmentEntryStatus; import org.olat.repository.RepositoryEntry; import org.springframework.beans.factory.annotation.Autowired; +import edu.emory.mathcs.backport.java.util.Collections; import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem; import uk.ac.ed.ph.jqtiplus.node.item.interaction.DrawingInteraction; import uk.ac.ed.ph.jqtiplus.node.item.interaction.ExtendedTextInteraction; @@ -79,6 +82,7 @@ public class CorrectionOverviewModel { private final Set<Identity> identityWithErrors = new HashSet<>(); private final Map<String,Boolean> manualCorrections = new ConcurrentHashMap<>(); + private final Map<Identity,String> anomyzedNamed; private Map<Identity,AssessmentTestSession> lastSessions; private final Map<AssessmentTestSession,Identity> reversedLastSessions = new HashMap<>(); private final Map<Identity,Boolean> assessedIdentitiesDone = new HashMap<>(); @@ -90,7 +94,8 @@ public class CorrectionOverviewModel { public CorrectionOverviewModel(RepositoryEntry courseEntry, IQTESTCourseNode courseNode, RepositoryEntry testEntry, ResolvedAssessmentTest resolvedAssessmentTest, ManifestBuilder manifestBuilder, - Map<Identity,AssessmentTestSession> lastSessions, Map<Identity, TestSessionState> testSessionStates) { + Map<Identity,AssessmentTestSession> lastSessions, Map<Identity, TestSessionState> testSessionStates, + Translator translator) { CoreSpringFactory.autowireObject(this); this.courseEntry = courseEntry; this.courseNode = courseNode; @@ -100,15 +105,18 @@ public class CorrectionOverviewModel { this.lastSessions = lastSessions; this.testSessionStates = testSessionStates; assessedIdentities = new ArrayList<>(lastSessions.keySet()); + Collections.shuffle(assessedIdentities); for(Map.Entry<Identity, AssessmentTestSession> entry:lastSessions.entrySet()) { reversedLastSessions.put(entry.getValue(), entry.getKey()); } + + anomyzedNamed = anonymize(translator); } public CorrectionOverviewModel(RepositoryEntry courseEntry, IQTESTCourseNode courseNode, RepositoryEntry testEntry, ResolvedAssessmentTest resolvedAssessmentTest, ManifestBuilder manifestBuilder, - List<Identity> assessedIdentities) { + List<Identity> assessedIdentities, Translator translator) { CoreSpringFactory.autowireObject(this); this.courseEntry = courseEntry; this.courseNode = courseNode; @@ -116,8 +124,36 @@ public class CorrectionOverviewModel { this.manifestBuilder = manifestBuilder; this.resolvedAssessmentTest = resolvedAssessmentTest; this.assessedIdentities = new ArrayList<>(assessedIdentities); + Collections.shuffle(this.assessedIdentities); + lastSessions = loadLastSessions(); testSessionStates = getTestSessionStates(lastSessions); + + anomyzedNamed = anonymize(translator); + } + + protected String getAnonymizedName(Identity identity) { + String name = anomyzedNamed.get(identity); + if(!StringHelper.containsNonWhitespace(name)) { + name = "UNKOWN"; + } + return name; + } + + private Map<Identity,String> anonymize(Translator translator) { + int count = 0; + + Map<Identity,String> names = new HashMap<>(); + for(Identity assessedIdentity:assessedIdentities) { + if(lastSessions.containsKey(assessedIdentity)) { + String title = translator.translate("number.assessed.identity", new String[] { Integer.toString(++count)} ); + names.put(assessedIdentity, title); + } else { + String title = translator.translate("number.assessed.identity", new String[] { "ERR" } ); + names.put(assessedIdentity, title); + } + } + return Map.copyOf(names); } public String getSubIdent() { diff --git a/src/main/java/org/olat/modules/grading/ui/GradingAssignmentsListController.java b/src/main/java/org/olat/modules/grading/ui/GradingAssignmentsListController.java index d6acbd17a3554a49b1c7abb97140c69e9e0ad21b..2b6e15c41403f02c9700ebc3b67c29d2df390076 100644 --- a/src/main/java/org/olat/modules/grading/ui/GradingAssignmentsListController.java +++ b/src/main/java/org/olat/modules/grading/ui/GradingAssignmentsListController.java @@ -596,7 +596,7 @@ public class GradingAssignmentsListController extends FormBasicController implem Map<Identity, TestSessionState> testSessionStates = new HashMap<>(); testSessionStates.put(assessedIdentity, testSessionState); CorrectionOverviewModel model = new CorrectionOverviewModel(entry, courseNode, referenceEntry, - resolvedAssessmentTest, manifestBuilder, lastSessions, testSessionStates); + resolvedAssessmentTest, manifestBuilder, lastSessions, testSessionStates, getTranslator()); GradingTimeRecordRef record = gradingService.getCurrentTimeRecord(assignment, ureq.getRequestTimestamp()); correctionCtrl = new CorrectionIdentityAssessmentItemListController(ureq, getWindowControl(), stackPanel, diff --git a/src/main/java/org/olat/modules/video/VideoManager.java b/src/main/java/org/olat/modules/video/VideoManager.java index fb721458d0a82082f5cb245b1c2c54324f9bf92a..236855fd15831c9ec3c3e3ab0e03d6e45d669cda 100644 --- a/src/main/java/org/olat/modules/video/VideoManager.java +++ b/src/main/java/org/olat/modules/video/VideoManager.java @@ -522,6 +522,6 @@ public interface VideoManager { * @param frame resource * @return true if image proposal is mostly black */ - public boolean getFrameWithFilter(VFSLeaf video, int frameNumber, long duration, VFSLeaf frame); + public boolean getFrameWithFilter(VFSLeaf video, Size movieSize, int frameNumber, long duration, VFSLeaf frame); } \ No newline at end of file diff --git a/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java b/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java index e840fbb31ad66e0a820cee515779fbc0e7eb83ee..8fda24a363f2ce3ad2f237a35ad1d0103efd747d 100644 --- a/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java +++ b/src/main/java/org/olat/modules/video/manager/VideoManagerImpl.java @@ -51,6 +51,7 @@ import org.apache.logging.log4j.Logger; import org.jcodec.api.FrameGrab; import org.jcodec.common.io.FileChannelWrapper; import org.jcodec.common.io.NIOUtils; +import org.jcodec.common.model.Picture; import org.jcodec.scale.AWTUtil; import org.olat.core.commons.persistence.DB; import org.olat.core.commons.services.image.Crop; @@ -58,6 +59,7 @@ import org.olat.core.commons.services.image.ImageService; import org.olat.core.commons.services.image.Size; import org.olat.core.commons.services.vfs.VFSMetadata; import org.olat.core.commons.services.vfs.VFSRepositoryService; +import org.olat.core.commons.services.video.JCodecHelper; import org.olat.core.commons.services.video.MovieService; import org.olat.core.gui.translator.Translator; import org.olat.core.id.Identity; @@ -289,11 +291,15 @@ public class VideoManagerImpl implements VideoManager { public boolean getFrame(VFSLeaf video, int frameNumber, VFSLeaf frame) { File videoFile = ((LocalFileImpl)video).getBasefile(); + Size movieSize = movieService.getSize(video, FILETYPE_MP4); + try (FileChannelWrapper in = NIOUtils.readableChannel(videoFile)) { FrameGrab frameGrab = FrameGrab.createFrameGrab(in).seekToFrameSloppy(frameNumber); OutputStream frameOutputStream = frame.getOutputStream(false); - BufferedImage bufImg = AWTUtil.toBufferedImage(frameGrab.getNativeFrame()); + Picture picture = frameGrab.getNativeFrame(); + BufferedImage bufImg = AWTUtil.toBufferedImage(picture); + bufImg = JCodecHelper.scale(movieSize, picture, bufImg); ImageIO.write(bufImg, "JPG", frameOutputStream); // close everything to prevent resource leaks @@ -301,22 +307,24 @@ public class VideoManagerImpl implements VideoManager { return true; } catch (Exception | AssertionError e) { - log.error("Could not get frame::" + frameNumber + " for video::" + videoFile.getAbsolutePath(), e); + log.error("Could not get frame::{} for video::{}", frameNumber, videoFile.getAbsolutePath(), e); return false; } } @Override - public boolean getFrameWithFilter(VFSLeaf video, int frameNumber, long duration, VFSLeaf frame) { + public boolean getFrameWithFilter(VFSLeaf video, Size movieSize, int frameNumber, long duration, VFSLeaf frame) { File videoFile = ((LocalFileImpl)video).getBasefile(); BufferedImage bufImg = null; boolean imgBlack = true; int countBlack = 0; + try (FileChannelWrapper in = NIOUtils.readableChannel(videoFile)) { OutputStream frameOutputStream = frame.getOutputStream(false); FrameGrab frameGrab = FrameGrab.createFrameGrab(in).seekToFrameSloppy(frameNumber); - bufImg = AWTUtil.toBufferedImage(frameGrab.getNativeFrame()); + Picture picture = frameGrab.getNativeFrame(); + bufImg = AWTUtil.toBufferedImage(picture); int xmin = bufImg.getMinX(); int ymin = bufImg.getMinY(); @@ -340,6 +348,7 @@ public class VideoManagerImpl implements VideoManager { imgBlack = true; } else { imgBlack = false; + bufImg = JCodecHelper.scale(movieSize, picture, bufImg); ImageIO.write(bufImg, "JPG", frameOutputStream); } // avoid endless loop @@ -351,7 +360,7 @@ public class VideoManagerImpl implements VideoManager { return imgBlack; } catch (Exception | AssertionError e) { - log.error("Could not get frame::" + frameNumber + " for video::" + videoFile.getAbsolutePath(), e); + log.error("Could not get frame: {} for video: {}", frameNumber, videoFile.getAbsolutePath(), e); return false; } } @@ -404,7 +413,7 @@ public class VideoManagerImpl implements VideoManager { try { return (VideoMetadata) XStreamHelper.readObject(XStreamHelper.createXStreamInstance(), metaDataFile); } catch (Exception e) { - log.error("Error while parsing XStream file for videoResource::" + videoResource, e); + log.error("Error while parsing XStream file for videoResource::{}", videoResource, e); // return an empty, so at least it displays something and not an error VideoMetadata meta = new VideoMetadataImpl(); meta.setWidth(800); @@ -534,7 +543,7 @@ public class VideoManagerImpl implements VideoManager { DecimalFormat df = new DecimalFormat("#.##"); df.setRoundingMode(RoundingMode.FLOOR); String ratioCalculated = df.format(width / (height + 1.0)); - String ratioString = "unknown"; + String ratioString; switch (ratioCalculated) { case "1.2": @@ -1079,7 +1088,7 @@ public class VideoManagerImpl implements VideoManager { try(OutputStream bos = new BufferedOutputStream(webvtt.getOutputStream(false))) { FileUtils.save(bos, vttString.toString(), ENCODING); } catch (IOException e) { - log.error("chapter.vtt could not be saved for videoResource::" + videoResource, e); + log.error("chapter.vtt could not be saved for videoResource::{}", videoResource, e); } } diff --git a/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java b/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java index 0e7e18b7c9578ce97418e41b7dee9ce13b8e2b4f..ad18fd5d47fcc3711967752a6abeea3ff7033a95 100644 --- a/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java +++ b/src/main/java/org/olat/modules/video/manager/VideoTranscodingJob.java @@ -107,14 +107,14 @@ public class VideoTranscodingJob extends JobWithDB { for (VideoTranscoding videoTrans : videoTranscodings) { String transcoder = videoTrans.getTranscoder(); if (transcoder == null) { - log.info("Start transcoding video with resolution::" + videoTrans.getResolution() - + " for video resource::" + videoTrans.getVideoResource().getResourceableId()); + log.info("Start transcoding video with resolution: {} for video resource: {}", + videoTrans.getResolution(), videoTrans.getVideoResource().getResourceableId()); videoTrans.setTranscoder(VideoTranscoding.TRANSCODER_LOCAL); videoTranscoding = videoManager.updateVideoTranscoding(videoTrans); break; } else if (transcoder.equals(VideoTranscoding.TRANSCODER_LOCAL)) { - log.info("Continue with transcoding video with resolution::" + videoTrans.getResolution() - + " for video resource::" + videoTrans.getVideoResource().getResourceableId()); + log.info("Continue with transcoding video with resolution: {} for video resource: {}", + videoTrans.getResolution(), videoTrans.getVideoResource().getResourceableId()); videoTranscoding = videoTrans; break; } @@ -238,7 +238,7 @@ public class VideoTranscodingJob extends JobWithDB { if(exitCode == 0) { videoTranscoding.setStatus(VideoTranscoding.TRANSCODING_STATUS_DONE); } else { - log.error("Exit code " + videoTranscoding + ":" + exitCode); + log.error("Exit code {}:{}", videoTranscoding, exitCode); videoTranscoding.setStatus(VideoTranscoding.TRANSCODING_STATUS_ERROR); } videoTranscoding = videoManager.updateVideoTranscoding(videoTranscoding); @@ -269,7 +269,7 @@ public class VideoTranscodingJob extends JobWithDB { int end = line.indexOf("."); if (end != -1 && end < 5) { String percent = line.substring(2, end); - log.debug("Output: " + percent); + log.debug("Output: {}", percent); // update version file for UI try { videoTranscoding.setStatus(Integer.parseInt(percent)); @@ -303,7 +303,7 @@ public class VideoTranscodingJob extends JobWithDB { String line = null; while ((line = berr.readLine()) != null) { - log.debug("Error: " + line); + log.debug("Error: {}", line); } } catch (IOException e) { log.error("", e); diff --git a/src/main/java/org/olat/modules/video/ui/VideoDisplayController.java b/src/main/java/org/olat/modules/video/ui/VideoDisplayController.java index 8ec49630f97d5248f16c8b2c2d4a2822a4eb72bd..e4102f82d0e90a8d2ab26bea2e0286e3aff40d67 100644 --- a/src/main/java/org/olat/modules/video/ui/VideoDisplayController.java +++ b/src/main/java/org/olat/modules/video/ui/VideoDisplayController.java @@ -157,18 +157,12 @@ public class VideoDisplayController extends BasicController { videoMetadata = videoManager.getVideoMetadata(videoEntry.getOlatResource()); VFSLeaf video = videoManager.getMasterVideoFile(videoEntry.getOlatResource()); + if(videoMetadata != null && videoMetadata.getHeight() != 600 && videoMetadata.getWidth() != 800) { + // we exclude 800x600 because it's the default (unkown) size and in this case we let the browser estimate the size + mainVC.contextPut("height", videoMetadata.getHeight()); + mainVC.contextPut("width", videoMetadata.getWidth()); + } if(video != null || (videoMetadata != null && StringHelper.containsNonWhitespace(videoMetadata.getUrl()))) { - if(displayOptions.isAutoWidth()){ - mainVC.contextPut("height", 480); - mainVC.contextPut("width", "100%"); - } else if(videoMetadata != null) { - mainVC.contextPut("height", videoMetadata.getHeight()); - mainVC.contextPut("width", videoMetadata.getWidth()); - } else { - mainVC.contextPut("height", 480); - mainVC.contextPut("width", 640); - } - // Load users preferred version from GUI prefs UserSession usess = ureq.getUserSession(); Preferences guiPrefs = usess.getGuiPreferences(); diff --git a/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java b/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java index 33c37a40f01ec19204816343c0bea752097ac5e7..f0547e0d496d9bfe5bf854d2789daedeb5b85f6e 100644 --- a/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java +++ b/src/main/java/org/olat/modules/video/ui/VideoPosterSelectionForm.java @@ -28,6 +28,8 @@ import java.util.UUID; import javax.servlet.http.HttpServletRequest; import org.olat.core.commons.modules.bc.FolderEvent; +import org.olat.core.commons.services.image.Size; +import org.olat.core.commons.services.video.MovieService; import org.olat.core.dispatcher.mapper.Mapper; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.Component; @@ -47,6 +49,7 @@ import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSItem; import org.olat.core.util.vfs.VFSLeaf; import org.olat.core.util.vfs.VFSMediaResource; +import org.olat.modules.video.VideoFormat; import org.olat.modules.video.VideoManager; import org.olat.modules.video.VideoMeta; import org.olat.resource.OLATResource; @@ -64,12 +67,16 @@ public class VideoPosterSelectionForm extends BasicController { private VFSLeaf tmpFile; private VFSContainer tmpContainer; - @Autowired - private VideoManager videoManager; private final VelocityContainer proposalLayout; + private Size movieSize; private static final int STEP = 24; private final boolean hasProposals; + + @Autowired + private MovieService movieService; + @Autowired + private VideoManager videoManager; public VideoPosterSelectionForm(UserRequest ureq, WindowControl wControl, OLATResource videoResource, VideoMeta videoMetadata) { @@ -83,8 +90,14 @@ public class VideoPosterSelectionForm extends BasicController { if(StringHelper.containsNonWhitespace(videoMetadata.getUrl())) { videoFile = videoManager.downloadTmpVideo(videoResource, videoMetadata); tmpFile = videoFile;// delete temporary file + if(videoMetadata.getVideoFormat() == VideoFormat.m3u8) { + movieSize = movieService.getSize(videoFile, VideoFormat.mp4.name()); + } } else { videoFile = videoManager.getMasterVideoFile(videoResource); + if(videoMetadata.getVideoFormat() == VideoFormat.mp4) { + movieSize = movieService.getSize(videoFile, VideoFormat.mp4.name()); + } } List<String> proposals = generatePosterProposals(videoFile); @@ -116,7 +129,7 @@ public class VideoPosterSelectionForm extends BasicController { private List<String> generatePosterProposals(VFSLeaf videoFile) { long frames = videoManager.getVideoFrameCount(videoFile); - + long framesStepping = frames / 7; if(framesStepping == 0) { framesStepping = 256; @@ -135,7 +148,7 @@ public class VideoPosterSelectionForm extends BasicController { boolean imgBlack = true; for(int i=0; i<maxAdjust && imgBlack; i++) { - imgBlack = videoManager.getFrameWithFilter(videoFile, (currentFrame+adjust), frames, posterProposal); + imgBlack = videoManager.getFrameWithFilter(videoFile, movieSize, (currentFrame+adjust), frames, posterProposal); if (currentFrame + STEP <= frames) { adjust += STEP; diff --git a/src/main/java/org/olat/modules/video/ui/_content/video_run.html b/src/main/java/org/olat/modules/video/ui/_content/video_run.html index 8528151d48a547fa2abb866cbb91aab46ea4f39c..79c43779fcf2e7fbfb89d5f65283831bf29c78f8 100644 --- a/src/main/java/org/olat/modules/video/ui/_content/video_run.html +++ b/src/main/java/org/olat/modules/video/ui/_content/video_run.html @@ -38,6 +38,12 @@ stretching: 'responsive', alwaysShowControls: $alwaysShowControls, clickToPlayPause: $clickToPlayPause, + #if($r.isNotEmpty($height)) + videoHeight: $height, + #end + #if($r.isNotEmpty($width)) + videoWidth: $width, + #end hls: { path: '$r.staticLink("movie/mediaelementjs/hls/hls.min.js")', }, diff --git a/src/test/java/org/olat/core/util/FileUtilsTest.java b/src/test/java/org/olat/core/util/FileUtilsTest.java index 7d955c51e585247c58a4c2f90ceaaa4a8a1dcc1e..544a70aa7abed7d6c567b291ead1398e5c0701ea 100644 --- a/src/test/java/org/olat/core/util/FileUtilsTest.java +++ b/src/test/java/org/olat/core/util/FileUtilsTest.java @@ -72,6 +72,9 @@ public class FileUtilsTest { assertCleanedFilename("fgh:ghj", "fgh_ghj"); assertCleanedFilename("fgh,ghj", "fgh_ghj"); assertCleanedFilename("fgh=ghj", "fgh_ghj"); + assertCleanedFilename(".fgh.ghj", "fgh.ghj"); + assertCleanedFilename("...fgh.ghj", "fgh.ghj"); + assertCleanedFilename(".....fgh.ghj", "fgh.ghj"); } private void assertCleanedFilename(String raw, String expected) {